From 8fcfe96ce5908fa6cfdb93e18563402495e5998e Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Wed, 30 Oct 2024 14:12:54 +0100 Subject: [PATCH 001/162] feat: per frame data --- src/modules/csm/checkpoint.py | 5 +- src/modules/csm/csm.py | 73 +++++++--- src/modules/csm/log.py | 6 +- src/modules/csm/state.py | 145 ++++++++++++++------ tests/modules/csm/test_checkpoint.py | 14 +- tests/modules/csm/test_csm_module.py | 196 ++++++++++++++++++++++++--- tests/modules/csm/test_log.py | 61 +++++++-- tests/modules/csm/test_state.py | 149 ++++++++++++-------- 8 files changed, 496 insertions(+), 153 deletions(-) diff --git a/src/modules/csm/checkpoint.py b/src/modules/csm/checkpoint.py index 0efc326c6..b111fe197 100644 --- a/src/modules/csm/checkpoint.py +++ b/src/modules/csm/checkpoint.py @@ -143,6 +143,7 @@ def exec(self, checkpoint: FrameCheckpoint) -> int: for duty_epoch in unprocessed_epochs } self._process(unprocessed_epochs, duty_epochs_roots) + self.state.commit() return len(unprocessed_epochs) def _get_block_roots(self, checkpoint_slot: SlotNumber): @@ -208,14 +209,14 @@ def _check_duty( with lock: for committee in committees.values(): for validator_duty in committee: - self.state.inc( + self.state.increment_duty( + duty_epoch, validator_duty.index, included=validator_duty.included, ) if duty_epoch not in self.state.unprocessed_epochs: raise ValueError(f"Epoch {duty_epoch} is not in epochs that should be processed") self.state.add_processed_epoch(duty_epoch) - self.state.commit() self.state.log_progress() unprocessed_epochs = self.state.unprocessed_epochs CSM_UNPROCESSED_EPOCHS_COUNT.set(len(unprocessed_epochs)) diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index 543a276e2..3d3619a94 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -13,7 +13,7 @@ from src.metrics.prometheus.duration_meter import duration_meter from src.modules.csm.checkpoint import FrameCheckpointProcessor, FrameCheckpointsIterator, MinStepIsNotReached from src.modules.csm.log import FramePerfLog -from src.modules.csm.state import State +from src.modules.csm.state import State, Frame from src.modules.csm.tree import Tree from src.modules.csm.types import ReportData, Shares from src.modules.submodules.consensus import ConsensusModule @@ -29,10 +29,11 @@ SlotNumber, StakingModuleAddress, StakingModuleId, + ValidatorIndex, ) from src.utils.blockstamp import build_blockstamp from src.utils.cache import global_lru_cache as lru_cache -from src.utils.slot import get_next_non_missed_slot +from src.utils.slot import get_next_non_missed_slot, get_reference_blockstamp from src.utils.web3converter import Web3Converter from src.web3py.extensions.lido_validators import NodeOperatorId, StakingModule, ValidatorsByNodeOperator from src.web3py.types import Web3 @@ -101,12 +102,12 @@ def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: if (prev_cid is None) != (prev_root == ZERO_HASH): raise InconsistentData(f"Got inconsistent previous tree data: {prev_root=} {prev_cid=}") - distributed, shares, log = self.calculate_distribution(blockstamp) + distributed, shares, logs = self.calculate_distribution(blockstamp) if distributed != sum(shares.values()): raise InconsistentData(f"Invalid distribution: {sum(shares.values())=} != {distributed=}") - log_cid = self.publish_log(log) + log_cid = self.publish_log(logs) if not distributed and not shares: logger.info({"msg": "No shares distributed in the current frame"}) @@ -201,7 +202,7 @@ def collect_data(self, blockstamp: BlockStamp) -> bool: logger.info({"msg": "The starting epoch of the frame is not finalized yet"}) return False - self.state.migrate(l_epoch, r_epoch, consensus_version) + self.state.init_or_migrate(l_epoch, r_epoch, converter.frame_config.epochs_per_frame, consensus_version) self.state.log_progress() if self.state.is_fulfilled: @@ -227,17 +228,56 @@ def collect_data(self, blockstamp: BlockStamp) -> bool: def calculate_distribution( self, blockstamp: ReferenceBlockStamp - ) -> tuple[int, defaultdict[NodeOperatorId, int], FramePerfLog]: + ) -> tuple[int, defaultdict[NodeOperatorId, int], list[FramePerfLog]]: """Computes distribution of fee shares at the given timestamp""" - - network_avg_perf = self.state.get_network_aggr().perf - threshold = network_avg_perf - self.w3.csm.oracle.perf_leeway_bp(blockstamp.block_hash) / TOTAL_BASIS_POINTS operators_to_validators = self.module_validators_by_node_operators(blockstamp) + distributed = 0 + # Calculate share of each CSM node operator. + shares = defaultdict[NodeOperatorId, int](int) + logs: list[FramePerfLog] = [] + + for frame in self.state.data: + from_epoch, to_epoch = frame + logger.info({"msg": f"Calculating distribution for frame [{from_epoch};{to_epoch}]"}) + frame_blockstamp = blockstamp + if to_epoch != blockstamp.ref_epoch: + frame_blockstamp = self._get_ref_blockstamp_for_frame(blockstamp, to_epoch) + distributed_in_frame, shares_in_frame, log = self._calculate_distribution_in_frame( + frame_blockstamp, operators_to_validators, frame, distributed + ) + distributed += distributed_in_frame + for no_id, share in shares_in_frame.items(): + shares[no_id] += share + logs.append(log) + + return distributed, shares, logs + + def _get_ref_blockstamp_for_frame( + self, blockstamp: ReferenceBlockStamp, frame_ref_epoch: EpochNumber + ) -> ReferenceBlockStamp: + converter = self.converter(blockstamp) + return get_reference_blockstamp( + cc=self.w3.cc, + ref_slot=converter.get_epoch_last_slot(frame_ref_epoch), + ref_epoch=frame_ref_epoch, + last_finalized_slot_number=blockstamp.slot_number, + ) + + def _calculate_distribution_in_frame( + self, + blockstamp: ReferenceBlockStamp, + operators_to_validators: ValidatorsByNodeOperator, + frame: Frame, + distributed: int, + ): + network_perf = self.state.get_network_aggr(frame).perf + threshold = network_perf - self.w3.csm.oracle.perf_leeway_bp(blockstamp.block_hash) / TOTAL_BASIS_POINTS + # Build the map of the current distribution operators. distribution: dict[NodeOperatorId, int] = defaultdict(int) stuck_operators = self.stuck_operators(blockstamp) - log = FramePerfLog(blockstamp, self.state.frame, threshold) + log = FramePerfLog(blockstamp, frame, threshold) for (_, no_id), validators in operators_to_validators.items(): if no_id in stuck_operators: @@ -245,7 +285,7 @@ def calculate_distribution( continue for v in validators: - aggr = self.state.data.get(v.index) + aggr = self.state.data[frame].get(ValidatorIndex(int(v.index))) if aggr is None: # It's possible that the validator is not assigned to any duty, hence it's performance @@ -268,13 +308,12 @@ def calculate_distribution( # Calculate share of each CSM node operator. shares = defaultdict[NodeOperatorId, int](int) total = sum(p for p in distribution.values()) + to_distribute = self.w3.csm.fee_distributor.shares_to_distribute(blockstamp.block_hash) - distributed + log.distributable = to_distribute if not total: return 0, shares, log - to_distribute = self.w3.csm.fee_distributor.shares_to_distribute(blockstamp.block_hash) - log.distributable = to_distribute - for no_id, no_share in distribution.items(): if no_share: shares[no_id] = to_distribute * no_share // total @@ -348,9 +387,9 @@ def publish_tree(self, tree: Tree) -> CID: logger.info({"msg": "Tree dump uploaded to IPFS", "cid": repr(tree_cid)}) return tree_cid - def publish_log(self, log: FramePerfLog) -> CID: - log_cid = self.w3.ipfs.publish(log.encode()) - logger.info({"msg": "Frame log uploaded to IPFS", "cid": repr(log_cid)}) + def publish_log(self, logs: list[FramePerfLog]) -> CID: + log_cid = self.w3.ipfs.publish(FramePerfLog.encode(logs)) + logger.info({"msg": "Frame(s) log uploaded to IPFS", "cid": repr(log_cid)}) return log_cid @lru_cache(maxsize=1) diff --git a/src/modules/csm/log.py b/src/modules/csm/log.py index f89f4ef58..39832c8c0 100644 --- a/src/modules/csm/log.py +++ b/src/modules/csm/log.py @@ -12,6 +12,7 @@ class LogJSONEncoder(json.JSONEncoder): ... @dataclass class ValidatorFrameSummary: + # TODO: Should be renamed. Perf means different things in different contexts perf: AttestationsAccumulator = field(default_factory=AttestationsAccumulator) slashed: bool = False @@ -35,13 +36,14 @@ class FramePerfLog: default_factory=lambda: defaultdict(OperatorFrameSummary) ) - def encode(self) -> bytes: + @staticmethod + def encode(logs: list['FramePerfLog']) -> bytes: return ( LogJSONEncoder( indent=None, separators=(',', ':'), sort_keys=True, ) - .encode(asdict(self)) + .encode([asdict(log) for log in logs]) .encode() ) diff --git a/src/modules/csm/state.py b/src/modules/csm/state.py index 4373f5259..fd27a8d62 100644 --- a/src/modules/csm/state.py +++ b/src/modules/csm/state.py @@ -3,6 +3,7 @@ import pickle from collections import defaultdict from dataclasses import dataclass +from itertools import batched from pathlib import Path from typing import Self @@ -12,6 +13,8 @@ logger = logging.getLogger(__name__) +type Frame = tuple[EpochNumber, EpochNumber] + class InvalidState(ValueError): """State has data considered as invalid for a report""" @@ -43,18 +46,21 @@ class State: The state can be migrated to be used for another frame's report by calling the `migrate` method. """ - - data: defaultdict[ValidatorIndex, AttestationsAccumulator] + data: dict[Frame, defaultdict[ValidatorIndex, AttestationsAccumulator]] _epochs_to_process: tuple[EpochNumber, ...] _processed_epochs: set[EpochNumber] + _epochs_per_frame: int _consensus_version: int = 1 - def __init__(self, data: dict[ValidatorIndex, AttestationsAccumulator] | None = None) -> None: - self.data = defaultdict(AttestationsAccumulator, data or {}) + def __init__(self, data: dict[Frame, dict[ValidatorIndex, AttestationsAccumulator]] | None = None) -> None: + self.data = { + frame: defaultdict(AttestationsAccumulator, validators) for frame, validators in (data or {}).items() + } self._epochs_to_process = tuple() self._processed_epochs = set() + self._epochs_per_frame = 0 EXTENSION = ".pkl" @@ -89,14 +95,37 @@ def file(cls) -> Path: def buffer(self) -> Path: return self.file().with_suffix(".buf") + @property + def is_empty(self) -> bool: + return not self.data and not self._epochs_to_process and not self._processed_epochs + + @property + def unprocessed_epochs(self) -> set[EpochNumber]: + if not self._epochs_to_process: + raise ValueError("Epochs to process are not set") + diff = set(self._epochs_to_process) - self._processed_epochs + return diff + + @property + def is_fulfilled(self) -> bool: + return not self.unprocessed_epochs + def clear(self) -> None: - self.data = defaultdict(AttestationsAccumulator) + self.data = {} self._epochs_to_process = tuple() self._processed_epochs.clear() assert self.is_empty - def inc(self, key: ValidatorIndex, included: bool) -> None: - self.data[key].add_duty(included) + def find_frame(self, epoch: EpochNumber) -> Frame: + frames = self.data.keys() + for epoch_range in frames: + if epoch_range[0] <= epoch <= epoch_range[1]: + return epoch_range + raise ValueError(f"Epoch {epoch} is out of frames range: {frames}") + + def increment_duty(self, epoch: EpochNumber, val_index: ValidatorIndex, included: bool) -> None: + epoch_range = self.find_frame(epoch) + self.data[epoch_range][val_index].add_duty(included) def add_processed_epoch(self, epoch: EpochNumber) -> None: self._processed_epochs.add(epoch) @@ -104,7 +133,7 @@ def add_processed_epoch(self, epoch: EpochNumber) -> None: def log_progress(self) -> None: logger.info({"msg": f"Processed {len(self._processed_epochs)} of {len(self._epochs_to_process)} epochs"}) - def migrate(self, l_epoch: EpochNumber, r_epoch: EpochNumber, consensus_version: int): + def init_or_migrate(self, l_epoch: EpochNumber, r_epoch: EpochNumber, epochs_per_frame: int, consensus_version: int) -> None: if consensus_version != self._consensus_version: logger.warning( { @@ -114,17 +143,60 @@ def migrate(self, l_epoch: EpochNumber, r_epoch: EpochNumber, consensus_version: ) self.clear() - for state_epochs in (self._epochs_to_process, self._processed_epochs): - for epoch in state_epochs: - if epoch < l_epoch or epoch > r_epoch: - logger.warning({"msg": "Discarding invalidated state cache"}) - self.clear() - break + if not self.is_empty: + invalidated = self._migrate_or_invalidate(l_epoch, r_epoch, epochs_per_frame) + if invalidated: + self.clear() + self._fill_frames(l_epoch, r_epoch, epochs_per_frame) + self._epochs_per_frame = epochs_per_frame self._epochs_to_process = tuple(sequence(l_epoch, r_epoch)) self._consensus_version = consensus_version self.commit() + def _fill_frames(self, l_epoch: EpochNumber, r_epoch: EpochNumber, epochs_per_frame: int) -> None: + frames = self.calculate_frames(tuple(sequence(l_epoch, r_epoch)), epochs_per_frame) + for frame in frames: + self.data.setdefault(frame, defaultdict(AttestationsAccumulator)) + + def _migrate_or_invalidate(self, l_epoch: EpochNumber, r_epoch: EpochNumber, epochs_per_frame: int) -> bool: + current_frames = self.calculate_frames(self._epochs_to_process, self._epochs_per_frame) + new_frames = self.calculate_frames(tuple(sequence(l_epoch, r_epoch)), epochs_per_frame) + inv_msg = f"Discarding invalid state cache because of frames change. {current_frames=}, {new_frames=}" + + if self._invalidate_on_epoch_range_change(l_epoch, r_epoch): + logger.warning({"msg": inv_msg}) + return True + + frame_expanded = epochs_per_frame > self._epochs_per_frame + frame_shrunk = epochs_per_frame < self._epochs_per_frame + + has_single_frame = len(current_frames) == len(new_frames) == 1 + + if has_single_frame and frame_expanded: + current_frame, *_ = current_frames + new_frame, *_ = new_frames + self.data[new_frame] = self.data.pop(current_frame) + logger.info({"msg": f"Migrated state cache to a new frame. {current_frame=}, {new_frame=}"}) + return False + + if has_single_frame and frame_shrunk: + logger.warning({"msg": inv_msg}) + return True + + if not has_single_frame and frame_expanded or frame_shrunk: + logger.warning({"msg": inv_msg}) + return True + + return False + + def _invalidate_on_epoch_range_change(self, l_epoch: EpochNumber, r_epoch: EpochNumber) -> bool: + """Check if the epoch range has been invalidated.""" + for epoch_set in (self._epochs_to_process, self._processed_epochs): + if any(epoch < l_epoch or epoch > r_epoch for epoch in epoch_set): + return True + return False + def validate(self, l_epoch: EpochNumber, r_epoch: EpochNumber) -> None: if not self.is_fulfilled: raise InvalidState(f"State is not fulfilled. {self.unprocessed_epochs=}") @@ -135,34 +207,25 @@ def validate(self, l_epoch: EpochNumber, r_epoch: EpochNumber) -> None: for epoch in sequence(l_epoch, r_epoch): if epoch not in self._processed_epochs: - raise InvalidState(f"Epoch {epoch} should be processed") - - @property - def is_empty(self) -> bool: - return not self.data and not self._epochs_to_process and not self._processed_epochs - - @property - def unprocessed_epochs(self) -> set[EpochNumber]: - if not self._epochs_to_process: - raise ValueError("Epochs to process are not set") - diff = set(self._epochs_to_process) - self._processed_epochs - return diff - - @property - def is_fulfilled(self) -> bool: - return not self.unprocessed_epochs - - @property - def frame(self) -> tuple[EpochNumber, EpochNumber]: - if not self._epochs_to_process: - raise ValueError("Epochs to process are not set") - return min(self._epochs_to_process), max(self._epochs_to_process) - - def get_network_aggr(self) -> AttestationsAccumulator: - """Return `AttestationsAccumulator` over duties of all the network validators""" - + raise InvalidState(f"Epoch {epoch} missing in processed epochs") + + @staticmethod + def calculate_frames(epochs_to_process: tuple[EpochNumber, ...], epochs_per_frame: int) -> list[Frame]: + """Split epochs to process into frames of `epochs_per_frame` length""" + frames = [] + for frame_epochs in batched(epochs_to_process, epochs_per_frame): + if len(frame_epochs) < epochs_per_frame: + raise ValueError("Insufficient epochs to form a frame") + frames.append((frame_epochs[0], frame_epochs[-1])) + return frames + + def get_network_aggr(self, frame: Frame) -> AttestationsAccumulator: + # TODO: exclude `active_slashed` validators from the calculation included = assigned = 0 - for validator, acc in self.data.items(): + frame_data = self.data.get(frame) + if not frame_data: + raise ValueError(f"No data for frame {frame} to calculate network aggregate") + for validator, acc in frame_data.items(): if acc.included > acc.assigned: raise ValueError(f"Invalid accumulator: {validator=}, {acc=}") included += acc.included diff --git a/tests/modules/csm/test_checkpoint.py b/tests/modules/csm/test_checkpoint.py index 44f23735e..4b456ed03 100644 --- a/tests/modules/csm/test_checkpoint.py +++ b/tests/modules/csm/test_checkpoint.py @@ -326,7 +326,7 @@ def test_checkpoints_processor_no_eip7549_support( monkeypatch: pytest.MonkeyPatch, ): state = State() - state.migrate(EpochNumber(0), EpochNumber(255), 1) + state.init_or_migrate(EpochNumber(0), EpochNumber(255), 256, 1) processor = FrameCheckpointProcessor( consensus_client, state, @@ -354,7 +354,7 @@ def test_checkpoints_processor_check_duty( converter, ): state = State() - state.migrate(0, 255, 1) + state.init_or_migrate(0, 255, 256, 1) finalized_blockstamp = ... processor = FrameCheckpointProcessor( consensus_client, @@ -367,7 +367,7 @@ def test_checkpoints_processor_check_duty( assert len(state._processed_epochs) == 1 assert len(state._epochs_to_process) == 256 assert len(state.unprocessed_epochs) == 255 - assert len(state.data) == 2048 * 32 + assert len(state.data[(0, 255)]) == 2048 * 32 def test_checkpoints_processor_process( @@ -379,7 +379,7 @@ def test_checkpoints_processor_process( converter, ): state = State() - state.migrate(0, 255, 1) + state.init_or_migrate(0, 255, 256, 1) finalized_blockstamp = ... processor = FrameCheckpointProcessor( consensus_client, @@ -392,7 +392,7 @@ def test_checkpoints_processor_process( assert len(state._processed_epochs) == 2 assert len(state._epochs_to_process) == 256 assert len(state.unprocessed_epochs) == 254 - assert len(state.data) == 2048 * 32 + assert len(state.data[(0, 255)]) == 2048 * 32 def test_checkpoints_processor_exec( @@ -404,7 +404,7 @@ def test_checkpoints_processor_exec( converter, ): state = State() - state.migrate(0, 255, 1) + state.init_or_migrate(0, 255, 256, 1) finalized_blockstamp = ... processor = FrameCheckpointProcessor( consensus_client, @@ -418,4 +418,4 @@ def test_checkpoints_processor_exec( assert len(state._processed_epochs) == 2 assert len(state._epochs_to_process) == 256 assert len(state.unprocessed_epochs) == 254 - assert len(state.data) == 2048 * 32 + assert len(state.data[(0, 255)]) == 2048 * 32 diff --git a/tests/modules/csm/test_csm_module.py b/tests/modules/csm/test_csm_module.py index f74af8d69..cdb0c92c5 100644 --- a/tests/modules/csm/test_csm_module.py +++ b/tests/modules/csm/test_csm_module.py @@ -9,7 +9,7 @@ from src.constants import UINT64_MAX from src.modules.csm.csm import CSOracle -from src.modules.csm.state import AttestationsAccumulator, State +from src.modules.csm.state import AttestationsAccumulator, State, Frame from src.modules.csm.tree import Tree from src.modules.submodules.oracle_module import ModuleExecuteDelay from src.modules.submodules.types import CurrentFrame, ZERO_HASH @@ -166,26 +166,37 @@ def test_calculate_distribution(module: CSOracle, csm: CSM): ] ) + frame_0: Frame = (EpochNumber(0), EpochNumber(999)) + + module.state.init_or_migrate(*frame_0, epochs_per_frame=1000, consensus_version=1) module.state = State( { - ValidatorIndex(0): AttestationsAccumulator(included=200, assigned=200), # short on frame - ValidatorIndex(1): AttestationsAccumulator(included=1000, assigned=1000), - ValidatorIndex(2): AttestationsAccumulator(included=1000, assigned=1000), - ValidatorIndex(3): AttestationsAccumulator(included=999, assigned=1000), - ValidatorIndex(4): AttestationsAccumulator(included=900, assigned=1000), - ValidatorIndex(5): AttestationsAccumulator(included=500, assigned=1000), # underperforming - ValidatorIndex(6): AttestationsAccumulator(included=0, assigned=0), # underperforming - ValidatorIndex(7): AttestationsAccumulator(included=900, assigned=1000), - ValidatorIndex(8): AttestationsAccumulator(included=500, assigned=1000), # underperforming - # ValidatorIndex(9): AttestationsAggregate(included=0, assigned=0), # missing in state - ValidatorIndex(10): AttestationsAccumulator(included=1000, assigned=1000), - ValidatorIndex(11): AttestationsAccumulator(included=1000, assigned=1000), - ValidatorIndex(12): AttestationsAccumulator(included=1000, assigned=1000), + frame_0: { + ValidatorIndex(0): AttestationsAccumulator(included=200, assigned=200), # short on frame + ValidatorIndex(1): AttestationsAccumulator(included=1000, assigned=1000), + ValidatorIndex(2): AttestationsAccumulator(included=1000, assigned=1000), + ValidatorIndex(3): AttestationsAccumulator(included=999, assigned=1000), + ValidatorIndex(4): AttestationsAccumulator(included=900, assigned=1000), + ValidatorIndex(5): AttestationsAccumulator(included=500, assigned=1000), # underperforming + ValidatorIndex(6): AttestationsAccumulator(included=0, assigned=0), # underperforming + ValidatorIndex(7): AttestationsAccumulator(included=900, assigned=1000), + ValidatorIndex(8): AttestationsAccumulator(included=500, assigned=1000), # underperforming + # ValidatorIndex(9): AttestationsAggregate(included=0, assigned=0), # missing in state + ValidatorIndex(10): AttestationsAccumulator(included=1000, assigned=1000), + ValidatorIndex(11): AttestationsAccumulator(included=1000, assigned=1000), + ValidatorIndex(12): AttestationsAccumulator(included=1000, assigned=1000), + } } ) - module.state.migrate(EpochNumber(100), EpochNumber(500), 1) - _, shares, log = module.calculate_distribution(blockstamp=Mock()) + l_epoch, r_epoch = frame_0 + + frame_0_network_aggr = module.state.get_network_aggr(frame_0) + + blockstamp = ReferenceBlockStampFactory.build(slot_number=r_epoch * 32, ref_epoch=r_epoch, ref_slot=r_epoch * 32) + _, shares, logs = module.calculate_distribution(blockstamp=blockstamp) + + log, *_ = logs assert tuple(shares.items()) == ( (NodeOperatorId(0), 476), @@ -225,8 +236,157 @@ def test_calculate_distribution(module: CSOracle, csm: CSM): assert log.operators[NodeOperatorId(3)].distributed == 2380 assert log.operators[NodeOperatorId(6)].distributed == 2380 - assert log.frame == (100, 500) - assert log.threshold == module.state.get_network_aggr().perf - 0.05 + assert log.frame == frame_0 + assert log.threshold == frame_0_network_aggr.perf - 0.05 + + +def test_calculate_distribution_with_missed_with_two_frames(module: CSOracle, csm: CSM): + csm.oracle.perf_leeway_bp = Mock(return_value=500) + csm.fee_distributor.shares_to_distribute = Mock(side_effect=[10000, 20000]) + + module.module_validators_by_node_operators = Mock( + return_value={ + (None, NodeOperatorId(0)): [Mock(index=0, validator=Mock(slashed=False))], + (None, NodeOperatorId(1)): [Mock(index=1, validator=Mock(slashed=False))], + (None, NodeOperatorId(2)): [Mock(index=2, validator=Mock(slashed=False))], # stuck + (None, NodeOperatorId(3)): [Mock(index=3, validator=Mock(slashed=False))], + (None, NodeOperatorId(4)): [Mock(index=4, validator=Mock(slashed=False))], # stuck + (None, NodeOperatorId(5)): [ + Mock(index=5, validator=Mock(slashed=False)), + Mock(index=6, validator=Mock(slashed=False)), + ], + (None, NodeOperatorId(6)): [ + Mock(index=7, validator=Mock(slashed=False)), + Mock(index=8, validator=Mock(slashed=False)), + ], + (None, NodeOperatorId(7)): [Mock(index=9, validator=Mock(slashed=False))], + (None, NodeOperatorId(8)): [ + Mock(index=10, validator=Mock(slashed=False)), + Mock(index=11, validator=Mock(slashed=True)), + ], + (None, NodeOperatorId(9)): [Mock(index=12, validator=Mock(slashed=True))], + } + ) + + module.stuck_operators = Mock( + side_effect=[ + [ + NodeOperatorId(2), + NodeOperatorId(4), + ], + [ + NodeOperatorId(2), + NodeOperatorId(4), + ], + ] + ) + + module.state = State() + l_epoch, r_epoch = EpochNumber(0), EpochNumber(1999) + frame_0 = (0, 999) + frame_1 = (1000, 1999) + module.state.init_or_migrate(l_epoch, r_epoch, epochs_per_frame=1000, consensus_version=1) + module.state = State( + { + frame_0: { + ValidatorIndex(0): AttestationsAccumulator(included=200, assigned=200), # short on frame + ValidatorIndex(1): AttestationsAccumulator(included=1000, assigned=1000), + ValidatorIndex(2): AttestationsAccumulator(included=1000, assigned=1000), + ValidatorIndex(3): AttestationsAccumulator(included=999, assigned=1000), + ValidatorIndex(4): AttestationsAccumulator(included=900, assigned=1000), + ValidatorIndex(5): AttestationsAccumulator(included=500, assigned=1000), # underperforming + ValidatorIndex(6): AttestationsAccumulator(included=0, assigned=0), # underperforming + ValidatorIndex(7): AttestationsAccumulator(included=900, assigned=1000), + ValidatorIndex(8): AttestationsAccumulator(included=500, assigned=1000), # underperforming + # ValidatorIndex(9): AttestationsAggregate(included=0, assigned=0), # missing in state + ValidatorIndex(10): AttestationsAccumulator(included=1000, assigned=1000), + ValidatorIndex(11): AttestationsAccumulator(included=1000, assigned=1000), + ValidatorIndex(12): AttestationsAccumulator(included=1000, assigned=1000), + }, + frame_1: { + ValidatorIndex(0): AttestationsAccumulator(included=200, assigned=200), # short on frame + ValidatorIndex(1): AttestationsAccumulator(included=1000, assigned=1000), + ValidatorIndex(2): AttestationsAccumulator(included=1000, assigned=1000), + ValidatorIndex(3): AttestationsAccumulator(included=999, assigned=1000), + ValidatorIndex(4): AttestationsAccumulator(included=900, assigned=1000), + ValidatorIndex(5): AttestationsAccumulator(included=500, assigned=1000), # underperforming + ValidatorIndex(6): AttestationsAccumulator(included=0, assigned=0), # underperforming + ValidatorIndex(7): AttestationsAccumulator(included=900, assigned=1000), + ValidatorIndex(8): AttestationsAccumulator(included=500, assigned=1000), # underperforming + # ValidatorIndex(9): AttestationsAggregate(included=0, assigned=0), # missing in state + ValidatorIndex(10): AttestationsAccumulator(included=1000, assigned=1000), + ValidatorIndex(11): AttestationsAccumulator(included=1000, assigned=1000), + ValidatorIndex(12): AttestationsAccumulator(included=1000, assigned=1000), + }, + } + ) + module.w3.cc = Mock() + + module.converter = Mock( + side_effect=lambda _: Mock( + frame_config=FrameConfigFactory.build(epochs_per_frame=1000), + get_epoch_last_slot=lambda epoch: epoch * 32 + 31, + ) + ) + + module._get_ref_blockstamp_for_frame = Mock( + side_effect=[ + ReferenceBlockStampFactory.build( + slot_number=frame_0[1] * 32, ref_epoch=frame_0[1], ref_slot=frame_0[1] * 32 + ), + ReferenceBlockStampFactory.build(slot_number=r_epoch * 32, ref_epoch=r_epoch, ref_slot=r_epoch * 32), + ] + ) + + blockstamp = ReferenceBlockStampFactory.build(slot_number=r_epoch * 32, ref_epoch=r_epoch, ref_slot=r_epoch * 32) + distributed, shares, logs = module.calculate_distribution(blockstamp=blockstamp) + + assert distributed == 2 * 9_998 # because of the rounding + + assert tuple(shares.items()) == ( + (NodeOperatorId(0), 952), + (NodeOperatorId(1), 4761), + (NodeOperatorId(3), 4761), + (NodeOperatorId(6), 4761), + (NodeOperatorId(8), 4761), + ) + + assert len(logs) == 2 + + for log in logs: + + assert log.frame in module.state.data.keys() + assert log.threshold == module.state.get_network_aggr(log.frame).perf - 0.05 + + assert tuple(log.operators.keys()) == ( + NodeOperatorId(0), + NodeOperatorId(1), + NodeOperatorId(2), + NodeOperatorId(3), + NodeOperatorId(4), + NodeOperatorId(5), + NodeOperatorId(6), + # NodeOperatorId(7), # Missing in state + NodeOperatorId(8), + NodeOperatorId(9), + ) + + assert not log.operators[NodeOperatorId(1)].stuck + + assert log.operators[NodeOperatorId(2)].validators == {} + assert log.operators[NodeOperatorId(2)].stuck + assert log.operators[NodeOperatorId(4)].validators == {} + assert log.operators[NodeOperatorId(4)].stuck + + assert 5 in log.operators[NodeOperatorId(5)].validators + assert 6 in log.operators[NodeOperatorId(5)].validators + assert 7 in log.operators[NodeOperatorId(6)].validators + + assert log.operators[NodeOperatorId(0)].distributed == 476 + assert log.operators[NodeOperatorId(1)].distributed in [2380, 2381] + assert log.operators[NodeOperatorId(2)].distributed == 0 + assert log.operators[NodeOperatorId(3)].distributed in [2380, 2381] + assert log.operators[NodeOperatorId(6)].distributed in [2380, 2381] # Static functions you were dreaming of for so long. diff --git a/tests/modules/csm/test_log.py b/tests/modules/csm/test_log.py index de52ca9ef..61004e9ed 100644 --- a/tests/modules/csm/test_log.py +++ b/tests/modules/csm/test_log.py @@ -1,8 +1,7 @@ import json import pytest -from src.modules.csm.log import FramePerfLog -from src.modules.csm.state import AttestationsAccumulator +from src.modules.csm.log import FramePerfLog, AttestationsAccumulator from src.types import EpochNumber, NodeOperatorId, ReferenceBlockStamp from tests.factory.blockstamp import ReferenceBlockStampFactory @@ -33,16 +32,56 @@ def test_log_encode(log: FramePerfLog): log.operators[NodeOperatorId(42)].distributed = 17 log.operators[NodeOperatorId(0)].distributed = 0 - encoded = log.encode() + logs = [log] + + encoded = FramePerfLog.encode(logs) + + for decoded in json.loads(encoded): + assert decoded["operators"]["42"]["validators"]["41337"]["perf"]["assigned"] == 220 + assert decoded["operators"]["42"]["validators"]["41337"]["perf"]["included"] == 119 + assert decoded["operators"]["42"]["distributed"] == 17 + assert decoded["operators"]["0"]["distributed"] == 0 + + assert decoded["blockstamp"]["block_hash"] == log.blockstamp.block_hash + assert decoded["blockstamp"]["ref_slot"] == log.blockstamp.ref_slot + + assert decoded["threshold"] == log.threshold + assert decoded["frame"] == list(log.frame) + + +def test_logs_encode(): + log_0 = FramePerfLog(ReferenceBlockStampFactory.build(), (EpochNumber(100), EpochNumber(500))) + log_0.operators[NodeOperatorId(42)].validators["41337"].perf = AttestationsAccumulator(220, 119) + log_0.operators[NodeOperatorId(42)].distributed = 17 + log_0.operators[NodeOperatorId(0)].distributed = 0 + + log_1 = FramePerfLog(ReferenceBlockStampFactory.build(), (EpochNumber(500), EpochNumber(900))) + log_1.operators[NodeOperatorId(5)].validators["1234"].perf = AttestationsAccumulator(400, 399) + log_1.operators[NodeOperatorId(5)].distributed = 40 + log_1.operators[NodeOperatorId(18)].distributed = 0 + + logs = [log_0, log_1] + + encoded = FramePerfLog.encode(logs) + decoded = json.loads(encoded) - assert decoded["operators"]["42"]["validators"]["41337"]["perf"]["assigned"] == 220 - assert decoded["operators"]["42"]["validators"]["41337"]["perf"]["included"] == 119 - assert decoded["operators"]["42"]["distributed"] == 17 - assert decoded["operators"]["0"]["distributed"] == 0 + assert len(decoded) == 2 + + assert decoded[0]["operators"]["42"]["validators"]["41337"]["perf"]["assigned"] == 220 + assert decoded[0]["operators"]["42"]["validators"]["41337"]["perf"]["included"] == 119 + assert decoded[0]["operators"]["42"]["distributed"] == 17 + assert decoded[0]["operators"]["0"]["distributed"] == 0 + + assert decoded[1]["operators"]["5"]["validators"]["1234"]["perf"]["assigned"] == 400 + assert decoded[1]["operators"]["5"]["validators"]["1234"]["perf"]["included"] == 399 + assert decoded[1]["operators"]["5"]["distributed"] == 40 + assert decoded[1]["operators"]["18"]["distributed"] == 0 - assert decoded["blockstamp"]["block_hash"] == log.blockstamp.block_hash - assert decoded["blockstamp"]["ref_slot"] == log.blockstamp.ref_slot + for i, log in enumerate(logs): + assert decoded[i]["blockstamp"]["block_hash"] == log.blockstamp.block_hash + assert decoded[i]["blockstamp"]["ref_slot"] == log.blockstamp.ref_slot - assert decoded["threshold"] == log.threshold - assert decoded["frame"] == list(log.frame) + assert decoded[i]["threshold"] == log.threshold + assert decoded[i]["frame"] == list(log.frame) + assert decoded[i]["distributable"] == log.distributable diff --git a/tests/modules/csm/test_state.py b/tests/modules/csm/test_state.py index 7539f7d26..d781522e2 100644 --- a/tests/modules/csm/test_state.py +++ b/tests/modules/csm/test_state.py @@ -26,51 +26,43 @@ def test_attestation_aggregate_perf(): def test_state_avg_perf(): state = State() - assert state.get_network_aggr().perf == 0 + frame = (0, 999) - state = State( - { + with pytest.raises(ValueError): + state.get_network_aggr(frame) + + state = State() + state.init_or_migrate(*frame, 1000, 1) + state.data = { + frame: { ValidatorIndex(0): AttestationsAccumulator(included=0, assigned=0), ValidatorIndex(1): AttestationsAccumulator(included=0, assigned=0), } - ) + } - assert state.get_network_aggr().perf == 0 + assert state.get_network_aggr(frame).perf == 0 - state = State( - { + state.data = { + frame: { ValidatorIndex(0): AttestationsAccumulator(included=333, assigned=777), ValidatorIndex(1): AttestationsAccumulator(included=167, assigned=223), } - ) - - assert state.get_network_aggr().perf == 0.5 - + } -def test_state_frame(): - state = State() - - state.migrate(EpochNumber(100), EpochNumber(500), 1) - assert state.frame == (100, 500) - - state.migrate(EpochNumber(300), EpochNumber(301), 1) - assert state.frame == (300, 301) - - state.clear() - - with pytest.raises(ValueError, match="Epochs to process are not set"): - state.frame + assert state.get_network_aggr(frame).perf == 0.5 def test_state_attestations(): state = State( { - ValidatorIndex(0): AttestationsAccumulator(included=333, assigned=777), - ValidatorIndex(1): AttestationsAccumulator(included=167, assigned=223), + (0, 999): { + ValidatorIndex(0): AttestationsAccumulator(included=333, assigned=777), + ValidatorIndex(1): AttestationsAccumulator(included=167, assigned=223), + } } ) - network_aggr = state.get_network_aggr() + network_aggr = state.get_network_aggr((0, 999)) assert network_aggr.assigned == 1000 assert network_aggr.included == 500 @@ -79,8 +71,10 @@ def test_state_attestations(): def test_state_load(): orig = State( { - ValidatorIndex(0): AttestationsAccumulator(included=333, assigned=777), - ValidatorIndex(1): AttestationsAccumulator(included=167, assigned=223), + (0, 999): { + ValidatorIndex(0): AttestationsAccumulator(included=333, assigned=777), + ValidatorIndex(1): AttestationsAccumulator(included=167, assigned=223), + } } ) @@ -92,8 +86,10 @@ def test_state_load(): def test_state_clear(): state = State( { - ValidatorIndex(0): AttestationsAccumulator(included=333, assigned=777), - ValidatorIndex(1): AttestationsAccumulator(included=167, assigned=223), + (0, 999): { + ValidatorIndex(0): AttestationsAccumulator(included=333, assigned=777), + ValidatorIndex(1): AttestationsAccumulator(included=167, assigned=223), + } } ) @@ -113,27 +109,42 @@ def test_state_add_processed_epoch(): def test_state_inc(): + + frame_0 = (0, 999) + frame_1 = (1000, 1999) + state = State( { - ValidatorIndex(0): AttestationsAccumulator(included=0, assigned=0), - ValidatorIndex(1): AttestationsAccumulator(included=1, assigned=2), + frame_0: { + ValidatorIndex(0): AttestationsAccumulator(included=333, assigned=777), + ValidatorIndex(1): AttestationsAccumulator(included=167, assigned=223), + }, + frame_1: { + ValidatorIndex(0): AttestationsAccumulator(included=1, assigned=1), + ValidatorIndex(1): AttestationsAccumulator(included=0, assigned=1), + }, } ) - state.inc(ValidatorIndex(0), True) - state.inc(ValidatorIndex(0), False) + state.increment_duty(999, ValidatorIndex(0), True) + state.increment_duty(999, ValidatorIndex(0), False) + state.increment_duty(999, ValidatorIndex(1), True) + state.increment_duty(999, ValidatorIndex(1), True) + state.increment_duty(999, ValidatorIndex(1), False) + state.increment_duty(999, ValidatorIndex(2), True) - state.inc(ValidatorIndex(1), True) - state.inc(ValidatorIndex(1), True) - state.inc(ValidatorIndex(1), False) + state.increment_duty(1000, ValidatorIndex(2), False) - state.inc(ValidatorIndex(2), True) - state.inc(ValidatorIndex(2), False) + assert tuple(state.data[frame_0].values()) == ( + AttestationsAccumulator(included=334, assigned=779), + AttestationsAccumulator(included=169, assigned=226), + AttestationsAccumulator(included=1, assigned=1), + ) - assert tuple(state.data.values()) == ( - AttestationsAccumulator(included=1, assigned=2), - AttestationsAccumulator(included=3, assigned=5), - AttestationsAccumulator(included=1, assigned=2), + assert tuple(state.data[frame_1].values()) == ( + AttestationsAccumulator(included=1, assigned=1), + AttestationsAccumulator(included=0, assigned=1), + AttestationsAccumulator(included=0, assigned=1), ) @@ -155,7 +166,7 @@ def test_empty_to_new_frame(self): l_epoch = EpochNumber(1) r_epoch = EpochNumber(255) - state.migrate(l_epoch, r_epoch, 1) + state.init_or_migrate(l_epoch, r_epoch, 255, 1) assert not state.is_empty assert state.unprocessed_epochs == set(sequence(l_epoch, r_epoch)) @@ -171,32 +182,60 @@ def test_empty_to_new_frame(self): def test_new_frame_requires_discarding_state(self, l_epoch_old, r_epoch_old, l_epoch_new, r_epoch_new): state = State() state.clear = Mock(side_effect=state.clear) - state.migrate(l_epoch_old, r_epoch_old, 1) + state.init_or_migrate(l_epoch_old, r_epoch_old, r_epoch_old - l_epoch_old + 1, 1) state.clear.assert_not_called() - state.migrate(l_epoch_new, r_epoch_new, 1) + state.init_or_migrate(l_epoch_new, r_epoch_new, r_epoch_new - l_epoch_new + 1, 1) state.clear.assert_called_once() assert state.unprocessed_epochs == set(sequence(l_epoch_new, r_epoch_new)) @pytest.mark.parametrize( - ("l_epoch_old", "r_epoch_old", "l_epoch_new", "r_epoch_new"), + ("l_epoch_old", "r_epoch_old", "l_epoch_new", "r_epoch_new", "epochs_per_frame"), + [ + pytest.param(1, 255, 1, 510, 255, id="Migrate Aa..b..B"), + ], + ) + def test_new_frame_extends_old_state(self, l_epoch_old, r_epoch_old, l_epoch_new, r_epoch_new, epochs_per_frame): + state = State() + state.clear = Mock(side_effect=state.clear) + + state.init_or_migrate(l_epoch_old, r_epoch_old, epochs_per_frame, 1) + state.clear.assert_not_called() + + state.init_or_migrate(l_epoch_new, r_epoch_new, epochs_per_frame, 1) + state.clear.assert_not_called() + + assert state.unprocessed_epochs == set(sequence(l_epoch_new, r_epoch_new)) + assert len(state.data) == 2 + assert list(state.data.keys()) == [(l_epoch_old, r_epoch_old), (r_epoch_old + 1, r_epoch_new)] + assert state.calculate_frames(state._epochs_to_process, epochs_per_frame) == [ + (l_epoch_old, r_epoch_old), + (r_epoch_old + 1, r_epoch_new), + ] + + @pytest.mark.parametrize( + ("l_epoch_old", "r_epoch_old", "epochs_per_frame_old", "l_epoch_new", "r_epoch_new", "epochs_per_frame_new"), [ - pytest.param(1, 255, 1, 510, id="Migrate Aa..b..B"), - pytest.param(32, 510, 1, 510, id="Migrate: A..a..b..B"), + pytest.param(32, 510, 479, 1, 510, 510, id="Migrate: A..a..b..B"), ], ) - def test_new_frame_extends_old_state(self, l_epoch_old, r_epoch_old, l_epoch_new, r_epoch_new): + def test_new_frame_extends_old_state_with_single_frame( + self, l_epoch_old, r_epoch_old, epochs_per_frame_old, l_epoch_new, r_epoch_new, epochs_per_frame_new + ): state = State() state.clear = Mock(side_effect=state.clear) - state.migrate(l_epoch_old, r_epoch_old, 1) + state.init_or_migrate(l_epoch_old, r_epoch_old, epochs_per_frame_old, 1) state.clear.assert_not_called() - state.migrate(l_epoch_new, r_epoch_new, 1) + state.init_or_migrate(l_epoch_new, r_epoch_new, epochs_per_frame_new, 1) state.clear.assert_not_called() assert state.unprocessed_epochs == set(sequence(l_epoch_new, r_epoch_new)) + assert len(state.data) == 1 + assert list(state.data.keys())[0] == (l_epoch_new, r_epoch_new) + assert state.calculate_frames(state._epochs_to_process, epochs_per_frame_new) == [(l_epoch_new, r_epoch_new)] @pytest.mark.parametrize( ("old_version", "new_version"), @@ -212,8 +251,8 @@ def test_consensus_version_change(self, old_version, new_version): l_epoch = r_epoch = EpochNumber(255) - state.migrate(l_epoch, r_epoch, old_version) + state.init_or_migrate(l_epoch, r_epoch, 1, old_version) state.clear.assert_not_called() - state.migrate(l_epoch, r_epoch, new_version) + state.init_or_migrate(l_epoch, r_epoch, 1, new_version) state.clear.assert_called_once() From c437e20f37705c5572bb7cf873961cbb495eafaf Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Wed, 22 Jan 2025 11:39:15 +0100 Subject: [PATCH 002/162] feat: sync and proposal duties --- src/constants.py | 4 +- src/modules/csm/checkpoint.py | 236 +++++++++++++++++++++++---- src/modules/csm/csm.py | 55 +++++-- src/modules/csm/log.py | 8 +- src/modules/csm/state.py | 99 ++++++++--- src/providers/consensus/client.py | 43 ++++- src/providers/consensus/types.py | 18 ++ src/variables.py | 2 +- tests/modules/csm/test_checkpoint.py | 14 +- tests/modules/csm/test_csm_module.py | 80 ++++----- tests/modules/csm/test_log.py | 8 +- tests/modules/csm/test_state.py | 86 +++++----- 12 files changed, 489 insertions(+), 164 deletions(-) diff --git a/src/constants.py b/src/constants.py index ec7495d27..f920bb0ea 100644 --- a/src/constants.py +++ b/src/constants.py @@ -30,7 +30,9 @@ MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA = Gwei(2**7 * 10**9) MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT = Gwei(2**8 * 10**9) # https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#time-parameters -SLOTS_PER_HISTORICAL_ROOT = 2**13 # 8192 +SLOTS_PER_HISTORICAL_ROOT = 2**13 # 8192 +# https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/beacon-chain.md#sync-committee +EPOCHS_PER_SYNC_COMMITTEE_PERIOD = 256 # https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#withdrawals-processing MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP = 2**3 diff --git a/src/modules/csm/checkpoint.py b/src/modules/csm/checkpoint.py index b111fe197..74f5013b2 100644 --- a/src/modules/csm/checkpoint.py +++ b/src/modules/csm/checkpoint.py @@ -6,13 +6,16 @@ from typing import Iterable, Sequence, TypeGuard from src import variables -from src.constants import SLOTS_PER_HISTORICAL_ROOT -from src.metrics.prometheus.csm import CSM_MIN_UNPROCESSED_EPOCH, CSM_UNPROCESSED_EPOCHS_COUNT +from src.constants import SLOTS_PER_HISTORICAL_ROOT, EPOCHS_PER_SYNC_COMMITTEE_PERIOD +from src.metrics.prometheus.csm import CSM_UNPROCESSED_EPOCHS_COUNT, CSM_MIN_UNPROCESSED_EPOCH from src.modules.csm.state import State from src.providers.consensus.client import ConsensusClient +from src.providers.consensus.types import SyncCommittee, SyncAggregate +from src.utils.blockstamp import build_blockstamp from src.providers.consensus.types import BlockAttestation, BlockAttestationEIP7549 from src.types import BlockRoot, BlockStamp, CommitteeIndex, EpochNumber, SlotNumber, ValidatorIndex from src.utils.range import sequence +from src.utils.slot import get_prev_non_missed_slot from src.utils.timeit import timeit from src.utils.types import hex_str_to_bytes from src.utils.web3converter import Web3Converter @@ -22,6 +25,7 @@ class MinStepIsNotReached(Exception): ... +class SlotOutOfRootsRange(Exception): ... @dataclass @@ -103,7 +107,24 @@ def _is_min_step_reached(self): return False -type Committees = dict[tuple[SlotNumber, CommitteeIndex], list[ValidatorDuty]] +type Slot = str +type CommitteeIndex = str +type SlotBlockRoot = tuple[SlotNumber, BlockRoot | None] +type SyncCommittees = dict[SlotNumber, list[ValidatorDuty]] +type AttestationCommittees = dict[tuple[Slot, CommitteeIndex], list[ValidatorDuty]] + + +class SyncCommitteesCache(dict): + + max_size = variables.CSM_ORACLE_MAX_CONCURRENCY + + def __setitem__(self, committee_network_index: int, value: SyncCommittee | None): + if len(self) >= self.max_size: + self.pop(min(self)) + super().__setitem__(committee_network_index, value) + + +SYNC_COMMITTEE_CACHE = SyncCommitteesCache() class FrameCheckpointProcessor: @@ -139,10 +160,10 @@ def exec(self, checkpoint: FrameCheckpoint) -> int: return 0 block_roots = self._get_block_roots(checkpoint.slot) duty_epochs_roots = { - duty_epoch: self._select_block_roots(duty_epoch, block_roots, checkpoint.slot) + duty_epoch: self._select_block_roots(block_roots, duty_epoch, checkpoint.slot) for duty_epoch in unprocessed_epochs } - self._process(unprocessed_epochs, duty_epochs_roots) + self._process(block_roots, checkpoint.slot, unprocessed_epochs, duty_epochs_roots) self.state.commit() return len(unprocessed_epochs) @@ -158,8 +179,8 @@ def _get_block_roots(self, checkpoint_slot: SlotNumber): return [br[i] if i == pivot_index or br[i] != br[i - 1] else None for i in range(len(br))] def _select_block_roots( - self, duty_epoch: EpochNumber, block_roots: list[BlockRoot | None], checkpoint_slot: SlotNumber - ) -> list[BlockRoot]: + self, block_roots: list[BlockRoot | None], duty_epoch: EpochNumber, checkpoint_slot: SlotNumber + ) -> tuple[list[SlotBlockRoot], list[SlotBlockRoot]]: roots_to_check = [] # To check duties in the current epoch you need to # have 32 slots of the current epoch and 32 slots of the next epoch @@ -168,20 +189,38 @@ def _select_block_roots( self.converter.get_epoch_last_slot(EpochNumber(duty_epoch + 1)), ) for slot_to_check in slots: - # From spec - # https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#get_block_root_at_slot - if not slot_to_check < checkpoint_slot <= slot_to_check + SLOTS_PER_HISTORICAL_ROOT: - raise ValueError("Slot is out of the state block roots range") - if br := block_roots[slot_to_check % SLOTS_PER_HISTORICAL_ROOT]: - roots_to_check.append(br) + block_root = self._select_block_root_by_slot(block_roots, checkpoint_slot, slot_to_check) + roots_to_check.append((slot_to_check, block_root)) + + duty_epoch_roots, next_epoch_roots = roots_to_check[:32], roots_to_check[32:] - return roots_to_check + return duty_epoch_roots, next_epoch_roots - def _process(self, unprocessed_epochs: list[EpochNumber], duty_epochs_roots: dict[EpochNumber, list[BlockRoot]]): + @staticmethod + def _select_block_root_by_slot(block_roots: list[BlockRoot | None], checkpoint_slot: SlotNumber, root_slot: SlotNumber) -> BlockRoot | None: + # From spec + # https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#get_block_root_at_slot + if not root_slot < checkpoint_slot <= root_slot + SLOTS_PER_HISTORICAL_ROOT: + raise SlotOutOfRootsRange("Slot is out of the state block roots range") + return block_roots[root_slot % SLOTS_PER_HISTORICAL_ROOT] + + def _process( + self, + checkpoint_block_roots: list[BlockRoot | None], + checkpoint_slot: SlotNumber, + unprocessed_epochs: list[EpochNumber], + duty_epochs_roots: dict[EpochNumber, tuple[list[SlotBlockRoot], list[SlotBlockRoot]]] + ): executor = ThreadPoolExecutor(max_workers=variables.CSM_ORACLE_MAX_CONCURRENCY) try: futures = { - executor.submit(self._check_duty, duty_epoch, duty_epochs_roots[duty_epoch]) + executor.submit( + self._check_duties, + checkpoint_block_roots, + checkpoint_slot, + duty_epoch, + *duty_epochs_roots[duty_epoch] + ) for duty_epoch in unprocessed_epochs } for future in as_completed(futures): @@ -195,27 +234,53 @@ def _process(self, unprocessed_epochs: list[EpochNumber], duty_epochs_roots: dic logger.info({"msg": "The executor was shut down"}) @timeit(lambda args, duration: logger.info({"msg": f"Epoch {args.duty_epoch} processed in {duration:.2f} seconds"})) - def _check_duty( + def _check_duties( self, + checkpoint_block_roots: list[BlockRoot | None], + checkpoint_slot: SlotNumber, duty_epoch: EpochNumber, - block_roots: list[BlockRoot], + duty_epoch_roots: list[SlotBlockRoot], + next_epoch_roots: list[SlotBlockRoot], ): logger.info({"msg": f"Processing epoch {duty_epoch}"}) - committees = self._prepare_committees(duty_epoch) - for root in block_roots: - attestations = self.cc.get_block_attestations(root) - process_attestations(attestations, committees, self.eip7549_supported) + + att_committees = self._prepare_att_committees(EpochNumber(duty_epoch)) + propose_duties = self._prepare_propose_duties(EpochNumber(duty_epoch), checkpoint_block_roots, checkpoint_slot) + sync_committees = self._prepare_sync_committee(EpochNumber(duty_epoch), duty_epoch_roots) + for slot, root in [*duty_epoch_roots, *next_epoch_roots]: + missed_slot = root is None + if missed_slot: + continue + attestations, sync_aggregate = self.cc.get_block_attestations_and_sync(BlockRoot(root)) + process_attestations(attestations, att_committees, self.eip7549_supported) + if (slot, root) in duty_epoch_roots: + propose_duties[slot].included = True + process_sync(slot, sync_aggregate, sync_committees) with lock: - for committee in committees.values(): - for validator_duty in committee: - self.state.increment_duty( - duty_epoch, - validator_duty.index, - included=validator_duty.included, - ) if duty_epoch not in self.state.unprocessed_epochs: raise ValueError(f"Epoch {duty_epoch} is not in epochs that should be processed") + frame = self.state.find_frame(duty_epoch) + for att_committee in att_committees.values(): + for att_duty in att_committee: + self.state.increment_att_duty( + frame, + att_duty.index, + included=att_duty.included, + ) + for sync_committee in sync_committees.values(): + for sync_duty in sync_committee: + self.state.increment_sync_duty( + frame, + sync_duty.index, + included=sync_duty.included, + ) + for proposer_duty in propose_duties.values(): + self.state.increment_prop_duty( + frame, + proposer_duty.index, + included=proposer_duty.included + ) self.state.add_processed_epoch(duty_epoch) self.state.log_progress() unprocessed_epochs = self.state.unprocessed_epochs @@ -224,10 +289,10 @@ def _check_duty( @timeit( lambda args, duration: logger.info( - {"msg": f"Committees for epoch {args.epoch} processed in {duration:.2f} seconds"} + {"msg": f"Attestation Committees for epoch {args.epoch} prepared in {duration:.2f} seconds"} ) ) - def _prepare_committees(self, epoch: EpochNumber) -> Committees: + def _prepare_att_committees(self, epoch: EpochNumber) -> AttestationCommittees: committees = {} for committee in self.cc.get_attestation_committees(self.finalized_blockstamp, epoch): validators = [] @@ -237,6 +302,115 @@ def _prepare_committees(self, epoch: EpochNumber) -> Committees: committees[(committee.slot, committee.index)] = validators return committees + @timeit( + lambda args, duration: logger.info( + {"msg": f"Sync Committee for epoch {args.epoch} prepared in {duration:.2f} seconds"} + ) + ) + def _prepare_sync_committee( + self, epoch: EpochNumber, duty_block_roots: list[SlotBlockRoot] + ) -> dict[SlotNumber, list[ValidatorDuty]]: + + sync_committee = self._get_cached_sync_committee(epoch) + + duties = {} + for slot, root in duty_block_roots: + missed_slot = root is None + if missed_slot: + continue + duties[slot] = [ + ValidatorDuty(index=ValidatorIndex(int(validator)), included=False) + for validator in sync_committee.validators + ] + + return duties + + def _get_cached_sync_committee(self, epoch: EpochNumber) -> SyncCommittee: + sync_committee_index = epoch // EPOCHS_PER_SYNC_COMMITTEE_PERIOD + with lock: + sync_committee = SYNC_COMMITTEE_CACHE.get(sync_committee_index) + if not sync_committee: + epochs_before_new_sync_committee = epoch % EPOCHS_PER_SYNC_COMMITTEE_PERIOD + epochs_range = EPOCHS_PER_SYNC_COMMITTEE_PERIOD - epochs_before_new_sync_committee + logger.info({"msg": f"Preparing cached Sync Committee for {epochs_range} epochs from {epoch} epoch"}) + state_blockstamp = build_blockstamp( + get_prev_non_missed_slot( + self.cc, + self.converter.get_epoch_first_slot(epoch), + self.finalized_blockstamp.slot_number + ) + ) + sync_committee = self.cc.get_sync_committee(state_blockstamp, epoch) + SYNC_COMMITTEE_CACHE[sync_committee_index] = sync_committee + return sync_committee + + @timeit( + lambda args, duration: logger.info( + {"msg": f"Propose Duties for epoch {args.epoch} prepared in {duration:.2f} seconds"} + ) + ) + def _prepare_propose_duties( + self, + epoch: EpochNumber, + checkpoint_block_roots: list[BlockRoot | None], + checkpoint_slot: SlotNumber + ) -> dict[SlotNumber, ValidatorDuty]: + duties = {} + dependent_root = self._get_dependent_root_for_proposer_duties(epoch, checkpoint_block_roots, checkpoint_slot) + proposer_duties = self.cc.get_proposer_duties(epoch, dependent_root) + for duty in proposer_duties: + duties[SlotNumber(int(duty.slot))] = ValidatorDuty( + index=ValidatorIndex(int(duty.validator_index)), included=False + ) + return duties + + def _get_dependent_root_for_proposer_duties( + self, + epoch: EpochNumber, + checkpoint_block_roots: list[BlockRoot | None], + checkpoint_slot: SlotNumber + ) -> BlockRoot: + dependent_root = None + dependent_slot = self.converter.get_epoch_last_slot(EpochNumber(epoch - 1)) + try: + while not dependent_root: + dependent_root = self._select_block_root_by_slot( + checkpoint_block_roots, checkpoint_slot, dependent_slot + ) + if dependent_root: + logger.debug( + { + "msg": f"Got dependent root from state block roots for epoch {epoch}. " + f"{dependent_slot=} {dependent_root=}" + } + ) + break + dependent_slot -= 1 + except SlotOutOfRootsRange: + dependent_non_missed_slot = SlotNumber(int( + get_prev_non_missed_slot( + self.cc, + dependent_slot, + self.finalized_blockstamp.slot_number + ).message.slot) + ) + dependent_root = self.cc.get_block_root(dependent_non_missed_slot).root + logger.debug( + { + "msg": f"Got dependent root from CL for epoch {epoch}. " + f"{dependent_non_missed_slot=} {dependent_root=}" + } + ) + return dependent_root + + +def process_sync(slot: SlotNumber, sync_aggregate: SyncAggregate, committees: SyncCommittees) -> None: + committee = committees[slot] + # Spec: https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/beacon-chain.md#syncaggregate + sync_bits = hex_bitvector_to_list(sync_aggregate.sync_committee_bits) + for index_in_committee in get_set_indices(sync_bits): + committee[index_in_committee].included = True + def process_attestations( attestations: Iterable[BlockAttestation], diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index 3d3619a94..c78b0f7cd 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -237,7 +237,8 @@ def calculate_distribution( shares = defaultdict[NodeOperatorId, int](int) logs: list[FramePerfLog] = [] - for frame in self.state.data: + frames = self.state.calculate_frames(self.state._epochs_to_process, self.state._epochs_per_frame) + for frame in frames: from_epoch, to_epoch = frame logger.info({"msg": f"Calculating distribution for frame [{from_epoch};{to_epoch}]"}) frame_blockstamp = blockstamp @@ -271,7 +272,15 @@ def _calculate_distribution_in_frame( frame: Frame, distributed: int, ): - network_perf = self.state.get_network_aggr(frame).perf + att_network_perf = self.state.get_att_network_aggr(frame).perf + prop_network_perf = self.state.get_prop_network_aggr(frame).perf + sync_network_perf = self.state.get_sync_network_aggr(frame).perf + + network_perf = 54/64 * att_network_perf + 8/64 * prop_network_perf + 2/64 * sync_network_perf + + if network_perf > 1: + raise ValueError(f"Invalid network performance: {network_perf=}") + threshold = network_perf - self.w3.csm.oracle.perf_leeway_bp(blockstamp.block_hash) / TOTAL_BASIS_POINTS # Build the map of the current distribution operators. @@ -280,30 +289,56 @@ def _calculate_distribution_in_frame( log = FramePerfLog(blockstamp, frame, threshold) for (_, no_id), validators in operators_to_validators.items(): + log_operator = log.operators[no_id] + if no_id in stuck_operators: - log.operators[no_id].stuck = True + log_operator.stuck = True continue for v in validators: - aggr = self.state.data[frame].get(ValidatorIndex(int(v.index))) + att_aggr = self.state.att_data[frame].get(ValidatorIndex(int(v.index))) + prop_aggr = self.state.prop_data[frame].get(ValidatorIndex(int(v.index))) + sync_aggr = self.state.sync_data[frame].get(ValidatorIndex(int(v.index))) - if aggr is None: + if att_aggr is None: # It's possible that the validator is not assigned to any duty, hence it's performance # is not presented in the aggregates (e.g. exited, pending for activation etc). + # TODO: check `sync_aggr` to strike (in case of bad sync performance) after validator exit continue + log_validator = log_operator.validators[v.index] + if v.validator.slashed is True: # It means that validator was active during the frame and got slashed and didn't meet the exit # epoch, so we should not count such validator for operator's share. - log.operators[no_id].validators[v.index].slashed = True + log_validator.slashed = True continue - if aggr.perf > threshold: + performance = att_aggr.perf + + if prop_aggr is not None and sync_aggr is not None: + performance = 54/64 * att_aggr.perf + 8/64 * prop_aggr.perf + 2/64 * sync_aggr.perf + + if prop_aggr is not None and sync_aggr is None: + performance = 54/62 * att_aggr.perf + 8/62 * prop_aggr.perf + + if prop_aggr is None and sync_aggr is not None: + performance = 54/56 * att_aggr.perf + 2/56 * sync_aggr.perf + + if performance > 1: + raise ValueError(f"Invalid performance: {performance=}") + + if performance > threshold: # Count of assigned attestations used as a metrics of time # the validator was active in the current frame. - distribution[no_id] += aggr.assigned - - log.operators[no_id].validators[v.index].perf = aggr + distribution[no_id] += att_aggr.assigned + + log_validator.performance = performance + log_validator.attestations = att_aggr + if prop_aggr is not None: + log_validator.proposals = prop_aggr + if sync_aggr is not None: + log_validator.sync_committee = sync_aggr # Calculate share of each CSM node operator. shares = defaultdict[NodeOperatorId, int](int) diff --git a/src/modules/csm/log.py b/src/modules/csm/log.py index 39832c8c0..746eed740 100644 --- a/src/modules/csm/log.py +++ b/src/modules/csm/log.py @@ -2,7 +2,7 @@ from collections import defaultdict from dataclasses import asdict, dataclass, field -from src.modules.csm.state import AttestationsAccumulator +from src.modules.csm.state import DutyAccumulator from src.modules.csm.types import Shares from src.types import EpochNumber, NodeOperatorId, ReferenceBlockStamp, ValidatorIndex @@ -12,8 +12,10 @@ class LogJSONEncoder(json.JSONEncoder): ... @dataclass class ValidatorFrameSummary: - # TODO: Should be renamed. Perf means different things in different contexts - perf: AttestationsAccumulator = field(default_factory=AttestationsAccumulator) + attestations: DutyAccumulator = field(default_factory=DutyAccumulator) + proposals: DutyAccumulator = field(default_factory=DutyAccumulator) + sync_committee: DutyAccumulator = field(default_factory=DutyAccumulator) + performance: float = 0.0 slashed: bool = False diff --git a/src/modules/csm/state.py b/src/modules/csm/state.py index fd27a8d62..a8e5faf4d 100644 --- a/src/modules/csm/state.py +++ b/src/modules/csm/state.py @@ -21,8 +21,8 @@ class InvalidState(ValueError): @dataclass -class AttestationsAccumulator: - """Accumulator of attestations duties observed for a validator""" +class DutyAccumulator: + """Accumulator of duties observed for a validator""" assigned: int = 0 included: int = 0 @@ -46,7 +46,9 @@ class State: The state can be migrated to be used for another frame's report by calling the `migrate` method. """ - data: dict[Frame, defaultdict[ValidatorIndex, AttestationsAccumulator]] + att_data: dict[Frame, defaultdict[ValidatorIndex, DutyAccumulator]] + prop_data: dict[Frame, defaultdict[ValidatorIndex, DutyAccumulator]] + sync_data: dict[Frame, defaultdict[ValidatorIndex, DutyAccumulator]] _epochs_to_process: tuple[EpochNumber, ...] _processed_epochs: set[EpochNumber] @@ -54,10 +56,12 @@ class State: _consensus_version: int = 1 - def __init__(self, data: dict[Frame, dict[ValidatorIndex, AttestationsAccumulator]] | None = None) -> None: - self.data = { - frame: defaultdict(AttestationsAccumulator, validators) for frame, validators in (data or {}).items() + def __init__(self, att_data: dict[Frame, dict[ValidatorIndex, DutyAccumulator]] | None = None) -> None: + self.att_data = { + frame: defaultdict(DutyAccumulator, validators) for frame, validators in (att_data or {}).items() } + self.prop_data = {} + self.sync_data = {} self._epochs_to_process = tuple() self._processed_epochs = set() self._epochs_per_frame = 0 @@ -97,7 +101,13 @@ def buffer(self) -> Path: @property def is_empty(self) -> bool: - return not self.data and not self._epochs_to_process and not self._processed_epochs + return ( + not self.att_data and + not self.sync_data and + not self.prop_data and + not self._epochs_to_process and + not self._processed_epochs + ) @property def unprocessed_epochs(self) -> set[EpochNumber]: @@ -111,21 +121,28 @@ def is_fulfilled(self) -> bool: return not self.unprocessed_epochs def clear(self) -> None: - self.data = {} + self.att_data = {} + self.sync_data = {} + self.prop_data = {} self._epochs_to_process = tuple() self._processed_epochs.clear() assert self.is_empty def find_frame(self, epoch: EpochNumber) -> Frame: - frames = self.data.keys() + frames = self.calculate_frames(self._epochs_to_process, self._epochs_per_frame) for epoch_range in frames: if epoch_range[0] <= epoch <= epoch_range[1]: return epoch_range raise ValueError(f"Epoch {epoch} is out of frames range: {frames}") - def increment_duty(self, epoch: EpochNumber, val_index: ValidatorIndex, included: bool) -> None: - epoch_range = self.find_frame(epoch) - self.data[epoch_range][val_index].add_duty(included) + def increment_att_duty(self, frame: Frame, val_index: ValidatorIndex, included: bool) -> None: + self.att_data[frame][val_index].add_duty(included) + + def increment_prop_duty(self, frame: Frame, val_index: ValidatorIndex, included: bool) -> None: + self.prop_data[frame][val_index].add_duty(included) + + def increment_sync_duty(self, frame: Frame, val_index: ValidatorIndex, included: bool) -> None: + self.sync_data[frame][val_index].add_duty(included) def add_processed_epoch(self, epoch: EpochNumber) -> None: self._processed_epochs.add(epoch) @@ -133,7 +150,13 @@ def add_processed_epoch(self, epoch: EpochNumber) -> None: def log_progress(self) -> None: logger.info({"msg": f"Processed {len(self._processed_epochs)} of {len(self._epochs_to_process)} epochs"}) - def init_or_migrate(self, l_epoch: EpochNumber, r_epoch: EpochNumber, epochs_per_frame: int, consensus_version: int) -> None: + def init_or_migrate( + self, + l_epoch: EpochNumber, + r_epoch: EpochNumber, + epochs_per_frame: int, + consensus_version: int + ) -> None: if consensus_version != self._consensus_version: logger.warning( { @@ -157,7 +180,9 @@ def init_or_migrate(self, l_epoch: EpochNumber, r_epoch: EpochNumber, epochs_per def _fill_frames(self, l_epoch: EpochNumber, r_epoch: EpochNumber, epochs_per_frame: int) -> None: frames = self.calculate_frames(tuple(sequence(l_epoch, r_epoch)), epochs_per_frame) for frame in frames: - self.data.setdefault(frame, defaultdict(AttestationsAccumulator)) + self.att_data.setdefault(frame, defaultdict(DutyAccumulator)) + self.prop_data.setdefault(frame, defaultdict(DutyAccumulator)) + self.sync_data.setdefault(frame, defaultdict(DutyAccumulator)) def _migrate_or_invalidate(self, l_epoch: EpochNumber, r_epoch: EpochNumber, epochs_per_frame: int) -> bool: current_frames = self.calculate_frames(self._epochs_to_process, self._epochs_per_frame) @@ -176,7 +201,9 @@ def _migrate_or_invalidate(self, l_epoch: EpochNumber, r_epoch: EpochNumber, epo if has_single_frame and frame_expanded: current_frame, *_ = current_frames new_frame, *_ = new_frames - self.data[new_frame] = self.data.pop(current_frame) + self.att_data[new_frame] = self.att_data.pop(current_frame) + self.prop_data[new_frame] = self.prop_data.pop(current_frame) + self.sync_data[new_frame] = self.sync_data.pop(current_frame) logger.info({"msg": f"Migrated state cache to a new frame. {current_frame=}, {new_frame=}"}) return False @@ -219,20 +246,54 @@ def calculate_frames(epochs_to_process: tuple[EpochNumber, ...], epochs_per_fram frames.append((frame_epochs[0], frame_epochs[-1])) return frames - def get_network_aggr(self, frame: Frame) -> AttestationsAccumulator: + def get_att_network_aggr(self, frame: Frame) -> DutyAccumulator: # TODO: exclude `active_slashed` validators from the calculation included = assigned = 0 - frame_data = self.data.get(frame) + frame_data = self.att_data.get(frame) if not frame_data: - raise ValueError(f"No data for frame {frame} to calculate network aggregate") + raise ValueError(f"No data for frame {frame} to calculate attestations network aggregate") for validator, acc in frame_data.items(): if acc.included > acc.assigned: raise ValueError(f"Invalid accumulator: {validator=}, {acc=}") included += acc.included assigned += acc.assigned - aggr = AttestationsAccumulator( + aggr = DutyAccumulator( included=included, assigned=assigned, ) logger.info({"msg": "Network attestations aggregate computed", "value": repr(aggr), "avg_perf": aggr.perf}) return aggr + + def get_sync_network_aggr(self, frame: Frame) -> DutyAccumulator: + included = assigned = 0 + frame_data = self.sync_data.get(frame) + if not frame_data: + raise ValueError(f"No data for frame {frame} to calculate syncs network aggregate") + for validator, acc in frame_data.items(): + if acc.included > acc.assigned: + raise ValueError(f"Invalid accumulator: {validator=}, {acc=}") + included += acc.included + assigned += acc.assigned + aggr = DutyAccumulator( + included=included, + assigned=assigned, + ) + logger.info({"msg": "Network syncs aggregate computed", "value": repr(aggr), "avg_perf": aggr.perf}) + return aggr + + def get_prop_network_aggr(self, frame: Frame) -> DutyAccumulator: + included = assigned = 0 + frame_data = self.prop_data.get(frame) + if not frame_data: + raise ValueError(f"No data for frame {frame} to calculate proposal network aggregate") + for validator, acc in frame_data.items(): + if acc.included > acc.assigned: + raise ValueError(f"Invalid accumulator: {validator=}, {acc=}") + included += acc.included + assigned += acc.assigned + aggr = DutyAccumulator( + included=included, + assigned=assigned, + ) + logger.info({"msg": "Network proposal aggregate computed", "value": repr(aggr), "avg_perf": aggr.perf}) + return aggr diff --git a/src/providers/consensus/client.py b/src/providers/consensus/client.py index 4b13d2ecc..022f4004d 100644 --- a/src/providers/consensus/client.py +++ b/src/providers/consensus/client.py @@ -17,6 +17,9 @@ BeaconSpecResponse, GenesisResponse, SlotAttestationCommittee, + ProposerDuties, + SyncCommittee, + SyncAggregate, ) from src.providers.http_provider import HTTPProvider, NotOkResponse from src.types import BlockRoot, BlockStamp, SlotNumber, EpochNumber, StateRoot @@ -48,6 +51,8 @@ class ConsensusClient(HTTPProvider): API_GET_BLOCK_HEADER = 'eth/v1/beacon/headers/{}' API_GET_BLOCK_DETAILS = 'eth/v2/beacon/blocks/{}' API_GET_ATTESTATION_COMMITTEES = 'eth/v1/beacon/states/{}/committees' + API_GET_SYNC_COMMITTEE = 'eth/v1/beacon/states/{}/sync_committees' + API_GET_PROPOSER_DUTIES = 'eth/v1/validator/duties/proposer/{}' API_GET_STATE = 'eth/v2/debug/beacon/states/{}' API_GET_VALIDATORS = 'eth/v1/beacon/states/{}/validators' API_GET_SPEC = 'eth/v1/config/spec' @@ -115,10 +120,7 @@ def get_block_details(self, state_id: SlotNumber | BlockRoot) -> BlockDetailsRes return BlockDetailsResponse.from_response(**data) @lru_cache(maxsize=256) - def get_block_attestations( - self, - state_id: SlotNumber | BlockRoot, - ) -> list[BlockAttestation]: + def get_block_attestations_and_sync(self, state_id: SlotNumber | BlockRoot) -> tuple[list[BlockAttestation], SyncAggregate]: """Spec: https://ethereum.github.io/beacon-APIs/#/Beacon/getBlockV2""" data, _ = self._get( self.API_GET_BLOCK_DETAILS, @@ -127,7 +129,11 @@ def get_block_attestations( ) if not isinstance(data, dict): raise ValueError("Expected mapping response from getBlockV2") - return [BlockAttestationResponse.from_response(**att) for att in data["message"]["body"]["attestations"]] + + attestations = [BlockAttestationResponse.from_response(**att) for att in data["message"]["body"]["attestations"]] + sync = SyncAggregate.from_response(**data["message"]["body"]["sync_aggregate"]) + + return attestations, sync @list_of_dataclasses(SlotAttestationCommittee.from_response) def get_attestation_committees( @@ -159,6 +165,33 @@ def get_attestation_committees( raise error return cast(list[SlotAttestationCommittee], data) + def get_sync_committee(self, blockstamp: BlockStamp, epoch: EpochNumber) -> SyncCommittee: + """Spec: https://ethereum.github.io/beacon-APIs/#/Beacon/getEpochSyncCommittees""" + data, _ = self._get( + self.API_GET_SYNC_COMMITTEE, + path_params=(blockstamp.state_root,), + query_params={'epoch': epoch}, + force_raise=self.__raise_on_prysm_error, + ) + if not isinstance(data, dict): + raise ValueError("Expected mapping response from getSyncCommittees") + return SyncCommittee.from_response(**data) + + @list_of_dataclasses(ProposerDuties.from_response) + def get_proposer_duties(self, epoch: EpochNumber, expected_dependent_root: BlockRoot) -> list[ProposerDuties]: + """Spec: https://ethereum.github.io/beacon-APIs/#/Validator/getProposerDuties""" + # It is recommended by spec to use the dependent root to ensure the epoch is correct + proposer_data, proposer_meta = self._get(self.API_GET_PROPOSER_DUTIES, path_params=(epoch,)) + if not isinstance(proposer_data, list): + raise ValueError("Expected list response from getProposerDuties") + response_dependent_root = proposer_meta['dependent_root'] + if response_dependent_root != expected_dependent_root: + raise ValueError( + "Dependent root for proposer duties request mismatch: " + f"{response_dependent_root=} is not {expected_dependent_root=}. Probably, CL node is not fully synced" + ) + return proposer_data + @lru_cache(maxsize=1) def get_state_block_roots(self, state_id: SlotNumber) -> list[BlockRoot]: streamed_json = cast(TransientStreamingJSONObject, self._get( diff --git a/src/providers/consensus/types.py b/src/providers/consensus/types.py index 70e2980c3..d0ed326fb 100644 --- a/src/providers/consensus/types.py +++ b/src/providers/consensus/types.py @@ -103,10 +103,16 @@ class BlockAttestationEIP7549(BlockAttestationPhase0): type BlockAttestation = BlockAttestationPhase0 | BlockAttestationEIP7549 +@dataclass +class SyncAggregate(FromResponse): + sync_committee_bits: str + + @dataclass class BeaconBlockBody(Nested, FromResponse): execution_payload: ExecutionPayload attestations: list[BlockAttestationResponse] + sync_aggregate: SyncAggregate @dataclass @@ -194,3 +200,15 @@ def indexed_validators(self) -> list[Validator]: ) for (i, v) in enumerate(self.validators) ] + + +@dataclass +class SyncCommittee(FromResponse): + validators: list[str] + + +@dataclass +class ProposerDuties(FromResponse): + pubkey: str + validator_index: str + slot: str diff --git a/src/variables.py b/src/variables.py index e4e36588c..a2b78b109 100644 --- a/src/variables.py +++ b/src/variables.py @@ -28,7 +28,7 @@ CSM_MODULE_ADDRESS: Final = os.getenv('CSM_MODULE_ADDRESS') FINALIZATION_BATCH_MAX_REQUEST_COUNT: Final = int(os.getenv('FINALIZATION_BATCH_MAX_REQUEST_COUNT', 1000)) EL_REQUESTS_BATCH_SIZE: Final = int(os.getenv('EL_REQUESTS_BATCH_SIZE', 500)) -CSM_ORACLE_MAX_CONCURRENCY: Final = int(os.getenv('CSM_ORACLE_MAX_CONCURRENCY', 2)) or None +CSM_ORACLE_MAX_CONCURRENCY: Final = min(32, (os.cpu_count() or 1) + 4, int(os.getenv('CSM_ORACLE_MAX_CONCURRENCY', 2))) # We add some gas to the transaction to be sure that we have enough gas to execute corner cases # eg when we tried to submit a few reports in a single block diff --git a/tests/modules/csm/test_checkpoint.py b/tests/modules/csm/test_checkpoint.py index 4b456ed03..94124f888 100644 --- a/tests/modules/csm/test_checkpoint.py +++ b/tests/modules/csm/test_checkpoint.py @@ -232,7 +232,7 @@ def test_checkpoints_processor_prepare_committees(mock_get_attestation_committee finalized_blockstamp, ) raw = consensus_client.get_attestation_committees(0, 0) - committees = processor._prepare_committees(0) + committees = processor._prepare_att_committees(0) assert len(committees) == 2048 for index, (committee_id, validators) in enumerate(committees.items()): slot, committee_index = committee_id @@ -253,7 +253,7 @@ def test_checkpoints_processor_process_attestations(mock_get_attestation_committ converter, finalized_blockstamp, ) - committees = processor._prepare_committees(0) + committees = processor._prepare_att_committees(0) # normal attestation attestation = cast(BlockAttestation, BlockAttestationFactory.build()) attestation.data.slot = 0 @@ -286,7 +286,7 @@ def test_checkpoints_processor_process_attestations_undefined_committee( converter, finalized_blockstamp, ) - committees = processor._prepare_committees(0) + committees = processor._prepare_att_committees(0) # undefined committee attestation = cast(BlockAttestation, BlockAttestationFactory.build()) attestation.data.slot = 100500 @@ -363,11 +363,11 @@ def test_checkpoints_processor_check_duty( finalized_blockstamp, ) roots = processor._get_block_roots(0) - processor._check_duty(0, roots[:64]) + processor._check_duties(0, roots[:64]) assert len(state._processed_epochs) == 1 assert len(state._epochs_to_process) == 256 assert len(state.unprocessed_epochs) == 255 - assert len(state.data[(0, 255)]) == 2048 * 32 + assert len(state.att_data[(0, 255)]) == 2048 * 32 def test_checkpoints_processor_process( @@ -392,7 +392,7 @@ def test_checkpoints_processor_process( assert len(state._processed_epochs) == 2 assert len(state._epochs_to_process) == 256 assert len(state.unprocessed_epochs) == 254 - assert len(state.data[(0, 255)]) == 2048 * 32 + assert len(state.att_data[(0, 255)]) == 2048 * 32 def test_checkpoints_processor_exec( @@ -418,4 +418,4 @@ def test_checkpoints_processor_exec( assert len(state._processed_epochs) == 2 assert len(state._epochs_to_process) == 256 assert len(state.unprocessed_epochs) == 254 - assert len(state.data[(0, 255)]) == 2048 * 32 + assert len(state.att_data[(0, 255)]) == 2048 * 32 diff --git a/tests/modules/csm/test_csm_module.py b/tests/modules/csm/test_csm_module.py index cdb0c92c5..a2eee25c5 100644 --- a/tests/modules/csm/test_csm_module.py +++ b/tests/modules/csm/test_csm_module.py @@ -9,7 +9,7 @@ from src.constants import UINT64_MAX from src.modules.csm.csm import CSOracle -from src.modules.csm.state import AttestationsAccumulator, State, Frame +from src.modules.csm.state import DutyAccumulator, State, Frame from src.modules.csm.tree import Tree from src.modules.submodules.oracle_module import ModuleExecuteDelay from src.modules.submodules.types import CurrentFrame, ZERO_HASH @@ -172,26 +172,26 @@ def test_calculate_distribution(module: CSOracle, csm: CSM): module.state = State( { frame_0: { - ValidatorIndex(0): AttestationsAccumulator(included=200, assigned=200), # short on frame - ValidatorIndex(1): AttestationsAccumulator(included=1000, assigned=1000), - ValidatorIndex(2): AttestationsAccumulator(included=1000, assigned=1000), - ValidatorIndex(3): AttestationsAccumulator(included=999, assigned=1000), - ValidatorIndex(4): AttestationsAccumulator(included=900, assigned=1000), - ValidatorIndex(5): AttestationsAccumulator(included=500, assigned=1000), # underperforming - ValidatorIndex(6): AttestationsAccumulator(included=0, assigned=0), # underperforming - ValidatorIndex(7): AttestationsAccumulator(included=900, assigned=1000), - ValidatorIndex(8): AttestationsAccumulator(included=500, assigned=1000), # underperforming + ValidatorIndex(0): DutyAccumulator(included=200, assigned=200), # short on frame + ValidatorIndex(1): DutyAccumulator(included=1000, assigned=1000), + ValidatorIndex(2): DutyAccumulator(included=1000, assigned=1000), + ValidatorIndex(3): DutyAccumulator(included=999, assigned=1000), + ValidatorIndex(4): DutyAccumulator(included=900, assigned=1000), + ValidatorIndex(5): DutyAccumulator(included=500, assigned=1000), # underperforming + ValidatorIndex(6): DutyAccumulator(included=0, assigned=0), # underperforming + ValidatorIndex(7): DutyAccumulator(included=900, assigned=1000), + ValidatorIndex(8): DutyAccumulator(included=500, assigned=1000), # underperforming # ValidatorIndex(9): AttestationsAggregate(included=0, assigned=0), # missing in state - ValidatorIndex(10): AttestationsAccumulator(included=1000, assigned=1000), - ValidatorIndex(11): AttestationsAccumulator(included=1000, assigned=1000), - ValidatorIndex(12): AttestationsAccumulator(included=1000, assigned=1000), + ValidatorIndex(10): DutyAccumulator(included=1000, assigned=1000), + ValidatorIndex(11): DutyAccumulator(included=1000, assigned=1000), + ValidatorIndex(12): DutyAccumulator(included=1000, assigned=1000), } } ) l_epoch, r_epoch = frame_0 - frame_0_network_aggr = module.state.get_network_aggr(frame_0) + frame_0_network_aggr = module.state.get_att_network_aggr(frame_0) blockstamp = ReferenceBlockStampFactory.build(slot_number=r_epoch * 32, ref_epoch=r_epoch, ref_slot=r_epoch * 32) _, shares, logs = module.calculate_distribution(blockstamp=blockstamp) @@ -289,34 +289,34 @@ def test_calculate_distribution_with_missed_with_two_frames(module: CSOracle, cs module.state = State( { frame_0: { - ValidatorIndex(0): AttestationsAccumulator(included=200, assigned=200), # short on frame - ValidatorIndex(1): AttestationsAccumulator(included=1000, assigned=1000), - ValidatorIndex(2): AttestationsAccumulator(included=1000, assigned=1000), - ValidatorIndex(3): AttestationsAccumulator(included=999, assigned=1000), - ValidatorIndex(4): AttestationsAccumulator(included=900, assigned=1000), - ValidatorIndex(5): AttestationsAccumulator(included=500, assigned=1000), # underperforming - ValidatorIndex(6): AttestationsAccumulator(included=0, assigned=0), # underperforming - ValidatorIndex(7): AttestationsAccumulator(included=900, assigned=1000), - ValidatorIndex(8): AttestationsAccumulator(included=500, assigned=1000), # underperforming + ValidatorIndex(0): DutyAccumulator(included=200, assigned=200), # short on frame + ValidatorIndex(1): DutyAccumulator(included=1000, assigned=1000), + ValidatorIndex(2): DutyAccumulator(included=1000, assigned=1000), + ValidatorIndex(3): DutyAccumulator(included=999, assigned=1000), + ValidatorIndex(4): DutyAccumulator(included=900, assigned=1000), + ValidatorIndex(5): DutyAccumulator(included=500, assigned=1000), # underperforming + ValidatorIndex(6): DutyAccumulator(included=0, assigned=0), # underperforming + ValidatorIndex(7): DutyAccumulator(included=900, assigned=1000), + ValidatorIndex(8): DutyAccumulator(included=500, assigned=1000), # underperforming # ValidatorIndex(9): AttestationsAggregate(included=0, assigned=0), # missing in state - ValidatorIndex(10): AttestationsAccumulator(included=1000, assigned=1000), - ValidatorIndex(11): AttestationsAccumulator(included=1000, assigned=1000), - ValidatorIndex(12): AttestationsAccumulator(included=1000, assigned=1000), + ValidatorIndex(10): DutyAccumulator(included=1000, assigned=1000), + ValidatorIndex(11): DutyAccumulator(included=1000, assigned=1000), + ValidatorIndex(12): DutyAccumulator(included=1000, assigned=1000), }, frame_1: { - ValidatorIndex(0): AttestationsAccumulator(included=200, assigned=200), # short on frame - ValidatorIndex(1): AttestationsAccumulator(included=1000, assigned=1000), - ValidatorIndex(2): AttestationsAccumulator(included=1000, assigned=1000), - ValidatorIndex(3): AttestationsAccumulator(included=999, assigned=1000), - ValidatorIndex(4): AttestationsAccumulator(included=900, assigned=1000), - ValidatorIndex(5): AttestationsAccumulator(included=500, assigned=1000), # underperforming - ValidatorIndex(6): AttestationsAccumulator(included=0, assigned=0), # underperforming - ValidatorIndex(7): AttestationsAccumulator(included=900, assigned=1000), - ValidatorIndex(8): AttestationsAccumulator(included=500, assigned=1000), # underperforming + ValidatorIndex(0): DutyAccumulator(included=200, assigned=200), # short on frame + ValidatorIndex(1): DutyAccumulator(included=1000, assigned=1000), + ValidatorIndex(2): DutyAccumulator(included=1000, assigned=1000), + ValidatorIndex(3): DutyAccumulator(included=999, assigned=1000), + ValidatorIndex(4): DutyAccumulator(included=900, assigned=1000), + ValidatorIndex(5): DutyAccumulator(included=500, assigned=1000), # underperforming + ValidatorIndex(6): DutyAccumulator(included=0, assigned=0), # underperforming + ValidatorIndex(7): DutyAccumulator(included=900, assigned=1000), + ValidatorIndex(8): DutyAccumulator(included=500, assigned=1000), # underperforming # ValidatorIndex(9): AttestationsAggregate(included=0, assigned=0), # missing in state - ValidatorIndex(10): AttestationsAccumulator(included=1000, assigned=1000), - ValidatorIndex(11): AttestationsAccumulator(included=1000, assigned=1000), - ValidatorIndex(12): AttestationsAccumulator(included=1000, assigned=1000), + ValidatorIndex(10): DutyAccumulator(included=1000, assigned=1000), + ValidatorIndex(11): DutyAccumulator(included=1000, assigned=1000), + ValidatorIndex(12): DutyAccumulator(included=1000, assigned=1000), }, } ) @@ -355,8 +355,8 @@ def test_calculate_distribution_with_missed_with_two_frames(module: CSOracle, cs for log in logs: - assert log.frame in module.state.data.keys() - assert log.threshold == module.state.get_network_aggr(log.frame).perf - 0.05 + assert log.frame in module.state.att_data.keys() + assert log.threshold == module.state.get_att_network_aggr(log.frame).perf - 0.05 assert tuple(log.operators.keys()) == ( NodeOperatorId(0), diff --git a/tests/modules/csm/test_log.py b/tests/modules/csm/test_log.py index 61004e9ed..7863ca57b 100644 --- a/tests/modules/csm/test_log.py +++ b/tests/modules/csm/test_log.py @@ -1,7 +1,7 @@ import json import pytest -from src.modules.csm.log import FramePerfLog, AttestationsAccumulator +from src.modules.csm.log import FramePerfLog, DutyAccumulator from src.types import EpochNumber, NodeOperatorId, ReferenceBlockStamp from tests.factory.blockstamp import ReferenceBlockStampFactory @@ -28,7 +28,7 @@ def test_fields_access(log: FramePerfLog): def test_log_encode(log: FramePerfLog): # Fill in dynamic fields to make sure we have data in it to be encoded. - log.operators[NodeOperatorId(42)].validators["41337"].perf = AttestationsAccumulator(220, 119) + log.operators[NodeOperatorId(42)].validators["41337"].perf = DutyAccumulator(220, 119) log.operators[NodeOperatorId(42)].distributed = 17 log.operators[NodeOperatorId(0)].distributed = 0 @@ -51,12 +51,12 @@ def test_log_encode(log: FramePerfLog): def test_logs_encode(): log_0 = FramePerfLog(ReferenceBlockStampFactory.build(), (EpochNumber(100), EpochNumber(500))) - log_0.operators[NodeOperatorId(42)].validators["41337"].perf = AttestationsAccumulator(220, 119) + log_0.operators[NodeOperatorId(42)].validators["41337"].perf = DutyAccumulator(220, 119) log_0.operators[NodeOperatorId(42)].distributed = 17 log_0.operators[NodeOperatorId(0)].distributed = 0 log_1 = FramePerfLog(ReferenceBlockStampFactory.build(), (EpochNumber(500), EpochNumber(900))) - log_1.operators[NodeOperatorId(5)].validators["1234"].perf = AttestationsAccumulator(400, 399) + log_1.operators[NodeOperatorId(5)].validators["1234"].perf = DutyAccumulator(400, 399) log_1.operators[NodeOperatorId(5)].distributed = 40 log_1.operators[NodeOperatorId(18)].distributed = 0 diff --git a/tests/modules/csm/test_state.py b/tests/modules/csm/test_state.py index d781522e2..0efa4fe96 100644 --- a/tests/modules/csm/test_state.py +++ b/tests/modules/csm/test_state.py @@ -3,7 +3,7 @@ import pytest -from src.modules.csm.state import AttestationsAccumulator, State +from src.modules.csm.state import DutyAccumulator, State from src.types import EpochNumber, ValidatorIndex from src.utils.range import sequence @@ -19,7 +19,7 @@ def mock_state_file(state_file_path: Path): def test_attestation_aggregate_perf(): - aggr = AttestationsAccumulator(included=333, assigned=777) + aggr = DutyAccumulator(included=333, assigned=777) assert aggr.perf == pytest.approx(0.4285, abs=1e-4) @@ -29,40 +29,40 @@ def test_state_avg_perf(): frame = (0, 999) with pytest.raises(ValueError): - state.get_network_aggr(frame) + state.get_att_network_aggr(frame) state = State() state.init_or_migrate(*frame, 1000, 1) - state.data = { + state.att_data = { frame: { - ValidatorIndex(0): AttestationsAccumulator(included=0, assigned=0), - ValidatorIndex(1): AttestationsAccumulator(included=0, assigned=0), + ValidatorIndex(0): DutyAccumulator(included=0, assigned=0), + ValidatorIndex(1): DutyAccumulator(included=0, assigned=0), } } - assert state.get_network_aggr(frame).perf == 0 + assert state.get_att_network_aggr(frame).perf == 0 - state.data = { + state.att_data = { frame: { - ValidatorIndex(0): AttestationsAccumulator(included=333, assigned=777), - ValidatorIndex(1): AttestationsAccumulator(included=167, assigned=223), + ValidatorIndex(0): DutyAccumulator(included=333, assigned=777), + ValidatorIndex(1): DutyAccumulator(included=167, assigned=223), } } - assert state.get_network_aggr(frame).perf == 0.5 + assert state.get_att_network_aggr(frame).perf == 0.5 def test_state_attestations(): state = State( { (0, 999): { - ValidatorIndex(0): AttestationsAccumulator(included=333, assigned=777), - ValidatorIndex(1): AttestationsAccumulator(included=167, assigned=223), + ValidatorIndex(0): DutyAccumulator(included=333, assigned=777), + ValidatorIndex(1): DutyAccumulator(included=167, assigned=223), } } ) - network_aggr = state.get_network_aggr((0, 999)) + network_aggr = state.get_att_network_aggr((0, 999)) assert network_aggr.assigned == 1000 assert network_aggr.included == 500 @@ -72,23 +72,23 @@ def test_state_load(): orig = State( { (0, 999): { - ValidatorIndex(0): AttestationsAccumulator(included=333, assigned=777), - ValidatorIndex(1): AttestationsAccumulator(included=167, assigned=223), + ValidatorIndex(0): DutyAccumulator(included=333, assigned=777), + ValidatorIndex(1): DutyAccumulator(included=167, assigned=223), } } ) orig.commit() copy = State.load() - assert copy.data == orig.data + assert copy.att_data == orig.att_data def test_state_clear(): state = State( { (0, 999): { - ValidatorIndex(0): AttestationsAccumulator(included=333, assigned=777), - ValidatorIndex(1): AttestationsAccumulator(included=167, assigned=223), + ValidatorIndex(0): DutyAccumulator(included=333, assigned=777), + ValidatorIndex(1): DutyAccumulator(included=167, assigned=223), } } ) @@ -98,7 +98,7 @@ def test_state_clear(): state.clear() assert state.is_empty - assert not state.data + assert not state.att_data def test_state_add_processed_epoch(): @@ -116,35 +116,35 @@ def test_state_inc(): state = State( { frame_0: { - ValidatorIndex(0): AttestationsAccumulator(included=333, assigned=777), - ValidatorIndex(1): AttestationsAccumulator(included=167, assigned=223), + ValidatorIndex(0): DutyAccumulator(included=333, assigned=777), + ValidatorIndex(1): DutyAccumulator(included=167, assigned=223), }, frame_1: { - ValidatorIndex(0): AttestationsAccumulator(included=1, assigned=1), - ValidatorIndex(1): AttestationsAccumulator(included=0, assigned=1), + ValidatorIndex(0): DutyAccumulator(included=1, assigned=1), + ValidatorIndex(1): DutyAccumulator(included=0, assigned=1), }, } ) - state.increment_duty(999, ValidatorIndex(0), True) - state.increment_duty(999, ValidatorIndex(0), False) - state.increment_duty(999, ValidatorIndex(1), True) - state.increment_duty(999, ValidatorIndex(1), True) - state.increment_duty(999, ValidatorIndex(1), False) - state.increment_duty(999, ValidatorIndex(2), True) + state.increment_att_duty(999, ValidatorIndex(0), True) + state.increment_att_duty(999, ValidatorIndex(0), False) + state.increment_att_duty(999, ValidatorIndex(1), True) + state.increment_att_duty(999, ValidatorIndex(1), True) + state.increment_att_duty(999, ValidatorIndex(1), False) + state.increment_att_duty(999, ValidatorIndex(2), True) - state.increment_duty(1000, ValidatorIndex(2), False) + state.increment_att_duty(1000, ValidatorIndex(2), False) - assert tuple(state.data[frame_0].values()) == ( - AttestationsAccumulator(included=334, assigned=779), - AttestationsAccumulator(included=169, assigned=226), - AttestationsAccumulator(included=1, assigned=1), + assert tuple(state.att_data[frame_0].values()) == ( + DutyAccumulator(included=334, assigned=779), + DutyAccumulator(included=169, assigned=226), + DutyAccumulator(included=1, assigned=1), ) - assert tuple(state.data[frame_1].values()) == ( - AttestationsAccumulator(included=1, assigned=1), - AttestationsAccumulator(included=0, assigned=1), - AttestationsAccumulator(included=0, assigned=1), + assert tuple(state.att_data[frame_1].values()) == ( + DutyAccumulator(included=1, assigned=1), + DutyAccumulator(included=0, assigned=1), + DutyAccumulator(included=0, assigned=1), ) @@ -207,8 +207,8 @@ def test_new_frame_extends_old_state(self, l_epoch_old, r_epoch_old, l_epoch_new state.clear.assert_not_called() assert state.unprocessed_epochs == set(sequence(l_epoch_new, r_epoch_new)) - assert len(state.data) == 2 - assert list(state.data.keys()) == [(l_epoch_old, r_epoch_old), (r_epoch_old + 1, r_epoch_new)] + assert len(state.att_data) == 2 + assert list(state.att_data.keys()) == [(l_epoch_old, r_epoch_old), (r_epoch_old + 1, r_epoch_new)] assert state.calculate_frames(state._epochs_to_process, epochs_per_frame) == [ (l_epoch_old, r_epoch_old), (r_epoch_old + 1, r_epoch_new), @@ -233,8 +233,8 @@ def test_new_frame_extends_old_state_with_single_frame( state.clear.assert_not_called() assert state.unprocessed_epochs == set(sequence(l_epoch_new, r_epoch_new)) - assert len(state.data) == 1 - assert list(state.data.keys())[0] == (l_epoch_new, r_epoch_new) + assert len(state.att_data) == 1 + assert list(state.att_data.keys())[0] == (l_epoch_new, r_epoch_new) assert state.calculate_frames(state._epochs_to_process, epochs_per_frame_new) == [(l_epoch_new, r_epoch_new)] @pytest.mark.parametrize( From eead54bd848a131a03d0b83a557d10caecd46643 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Mon, 3 Feb 2025 16:40:00 +0100 Subject: [PATCH 003/162] fix: type --- src/modules/csm/checkpoint.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/modules/csm/checkpoint.py b/src/modules/csm/checkpoint.py index 74f5013b2..b1b50b18e 100644 --- a/src/modules/csm/checkpoint.py +++ b/src/modules/csm/checkpoint.py @@ -107,11 +107,10 @@ def _is_min_step_reached(self): return False -type Slot = str type CommitteeIndex = str type SlotBlockRoot = tuple[SlotNumber, BlockRoot | None] type SyncCommittees = dict[SlotNumber, list[ValidatorDuty]] -type AttestationCommittees = dict[tuple[Slot, CommitteeIndex], list[ValidatorDuty]] +type AttestationCommittees = dict[tuple[SlotNumber, CommitteeIndex], list[ValidatorDuty]] class SyncCommitteesCache(dict): @@ -414,7 +413,7 @@ def process_sync(slot: SlotNumber, sync_aggregate: SyncAggregate, committees: Sy def process_attestations( attestations: Iterable[BlockAttestation], - committees: Committees, + committees: AttestationCommittees, eip7549_supported: bool = True, ) -> None: for attestation in attestations: From f48ce022c62951b5d96b15dc3f1fe8d550180edf Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Mon, 3 Feb 2025 17:31:37 +0100 Subject: [PATCH 004/162] fix: linter --- src/modules/checks/suites/consensus_node.py | 4 +- src/modules/csm/checkpoint.py | 5 +- src/modules/csm/csm.py | 158 ++++++++++++-------- src/modules/csm/state.py | 38 ++--- src/providers/consensus/client.py | 2 +- tests/modules/csm/test_state.py | 6 +- 6 files changed, 124 insertions(+), 89 deletions(-) diff --git a/src/modules/checks/suites/consensus_node.py b/src/modules/checks/suites/consensus_node.py index 39ed6f0db..a4994cda8 100644 --- a/src/modules/checks/suites/consensus_node.py +++ b/src/modules/checks/suites/consensus_node.py @@ -36,6 +36,6 @@ def check_attestation_committees(web3: Web3, blockstamp): assert web3.cc.get_attestation_committees(blockstamp, epoch), "consensus-client provide no attestation committees" -def check_block_attestations(web3: Web3, blockstamp): +def check_block_attestations_and_sync(web3: Web3, blockstamp): """Check that consensus-client able to provide block attestations""" - assert web3.cc.get_block_attestations(blockstamp.slot_number), "consensus-client provide no block attestations" + assert web3.cc.get_block_attestations_and_sync(blockstamp.slot_number), "consensus-client provide no block attestations and sync" diff --git a/src/modules/csm/checkpoint.py b/src/modules/csm/checkpoint.py index b1b50b18e..6672bf0df 100644 --- a/src/modules/csm/checkpoint.py +++ b/src/modules/csm/checkpoint.py @@ -107,7 +107,6 @@ def _is_min_step_reached(self): return False -type CommitteeIndex = str type SlotBlockRoot = tuple[SlotNumber, BlockRoot | None] type SyncCommittees = dict[SlotNumber, list[ValidatorDuty]] type AttestationCommittees = dict[tuple[SlotNumber, CommitteeIndex], list[ValidatorDuty]] @@ -250,7 +249,7 @@ def _check_duties( missed_slot = root is None if missed_slot: continue - attestations, sync_aggregate = self.cc.get_block_attestations_and_sync(BlockRoot(root)) + attestations, sync_aggregate = self.cc.get_block_attestations_and_sync(root) process_attestations(attestations, att_committees, self.eip7549_supported) if (slot, root) in duty_epoch_roots: propose_duties[slot].included = True @@ -384,7 +383,7 @@ def _get_dependent_root_for_proposer_duties( } ) break - dependent_slot -= 1 + dependent_slot = SlotNumber(int(dependent_slot - 1)) except SlotOutOfRootsRange: dependent_non_missed_slot = SlotNumber(int( get_prev_non_missed_slot( diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index c78b0f7cd..5cd5786d1 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -12,7 +12,7 @@ ) from src.metrics.prometheus.duration_meter import duration_meter from src.modules.csm.checkpoint import FrameCheckpointProcessor, FrameCheckpointsIterator, MinStepIsNotReached -from src.modules.csm.log import FramePerfLog +from src.modules.csm.log import FramePerfLog, OperatorFrameSummary from src.modules.csm.state import State, Frame from src.modules.csm.tree import Tree from src.modules.csm.types import ReportData, Shares @@ -29,13 +29,12 @@ SlotNumber, StakingModuleAddress, StakingModuleId, - ValidatorIndex, ) from src.utils.blockstamp import build_blockstamp from src.utils.cache import global_lru_cache as lru_cache from src.utils.slot import get_next_non_missed_slot, get_reference_blockstamp from src.utils.web3converter import Web3Converter -from src.web3py.extensions.lido_validators import NodeOperatorId, StakingModule, ValidatorsByNodeOperator +from src.web3py.extensions.lido_validators import NodeOperatorId, StakingModule, ValidatorsByNodeOperator, LidoValidator from src.web3py.types import Web3 logger = logging.getLogger(__name__) @@ -237,8 +236,7 @@ def calculate_distribution( shares = defaultdict[NodeOperatorId, int](int) logs: list[FramePerfLog] = [] - frames = self.state.calculate_frames(self.state._epochs_to_process, self.state._epochs_per_frame) - for frame in frames: + for frame in self.state.frames: from_epoch, to_epoch = frame logger.info({"msg": f"Calculating distribution for frame [{from_epoch};{to_epoch}]"}) frame_blockstamp = blockstamp @@ -272,16 +270,8 @@ def _calculate_distribution_in_frame( frame: Frame, distributed: int, ): - att_network_perf = self.state.get_att_network_aggr(frame).perf - prop_network_perf = self.state.get_prop_network_aggr(frame).perf - sync_network_perf = self.state.get_sync_network_aggr(frame).perf - - network_perf = 54/64 * att_network_perf + 8/64 * prop_network_perf + 2/64 * sync_network_perf - - if network_perf > 1: - raise ValueError(f"Invalid network performance: {network_perf=}") - - threshold = network_perf - self.w3.csm.oracle.perf_leeway_bp(blockstamp.block_hash) / TOTAL_BASIS_POINTS + network_perf = self._calculate_network_performance(frame) + threshold = self._calculate_threshold(network_perf, blockstamp) # Build the map of the current distribution operators. distribution: dict[NodeOperatorId, int] = defaultdict(int) @@ -289,60 +279,104 @@ def _calculate_distribution_in_frame( log = FramePerfLog(blockstamp, frame, threshold) for (_, no_id), validators in operators_to_validators.items(): - log_operator = log.operators[no_id] - - if no_id in stuck_operators: - log_operator.stuck = True - continue + self._process_operator(validators, no_id, stuck_operators, frame, log, distribution, threshold) - for v in validators: - att_aggr = self.state.att_data[frame].get(ValidatorIndex(int(v.index))) - prop_aggr = self.state.prop_data[frame].get(ValidatorIndex(int(v.index))) - sync_aggr = self.state.sync_data[frame].get(ValidatorIndex(int(v.index))) + return self._finalize_distribution(distribution, blockstamp, distributed, log) - if att_aggr is None: - # It's possible that the validator is not assigned to any duty, hence it's performance - # is not presented in the aggregates (e.g. exited, pending for activation etc). - # TODO: check `sync_aggr` to strike (in case of bad sync performance) after validator exit - continue + def _calculate_network_performance(self, frame: Frame) -> float: + att_perf = self.state.get_att_network_aggr(frame).perf + prop_perf = self.state.get_prop_network_aggr(frame).perf + sync_perf = self.state.get_sync_network_aggr(frame).perf + network_perf = 54 / 64 * att_perf + 8 / 64 * prop_perf + 2 / 64 * sync_perf - log_validator = log_operator.validators[v.index] - - if v.validator.slashed is True: - # It means that validator was active during the frame and got slashed and didn't meet the exit - # epoch, so we should not count such validator for operator's share. - log_validator.slashed = True - continue - - performance = att_aggr.perf - - if prop_aggr is not None and sync_aggr is not None: - performance = 54/64 * att_aggr.perf + 8/64 * prop_aggr.perf + 2/64 * sync_aggr.perf - - if prop_aggr is not None and sync_aggr is None: - performance = 54/62 * att_aggr.perf + 8/62 * prop_aggr.perf + if network_perf > 1: + raise ValueError(f"Invalid network performance: {network_perf=}") + return network_perf - if prop_aggr is None and sync_aggr is not None: - performance = 54/56 * att_aggr.perf + 2/56 * sync_aggr.perf + def _calculate_threshold(self, network_perf: float, blockstamp: ReferenceBlockStamp) -> float: + return network_perf - self.w3.csm.oracle.perf_leeway_bp(blockstamp.block_hash) / TOTAL_BASIS_POINTS - if performance > 1: - raise ValueError(f"Invalid performance: {performance=}") + def _process_operator( + self, + validators: list[LidoValidator], + no_id: NodeOperatorId, + stuck_operators: set[NodeOperatorId], + frame: Frame, + log: FramePerfLog, + distribution: dict[NodeOperatorId, int], + threshold: float + ): + log_operator = log.operators[no_id] - if performance > threshold: - # Count of assigned attestations used as a metrics of time - # the validator was active in the current frame. - distribution[no_id] += att_aggr.assigned + if no_id in stuck_operators: + log_operator.stuck = True + return - log_validator.performance = performance - log_validator.attestations = att_aggr - if prop_aggr is not None: - log_validator.proposals = prop_aggr - if sync_aggr is not None: - log_validator.sync_committee = sync_aggr + for v in validators: + self._process_validator(v, frame, log_operator, distribution, threshold) - # Calculate share of each CSM node operator. - shares = defaultdict[NodeOperatorId, int](int) - total = sum(p for p in distribution.values()) + def _process_validator( + self, + validator: LidoValidator, + frame: Frame, + log_operator: OperatorFrameSummary, + distribution: dict[NodeOperatorId, int], + threshold: float + ): + att_aggr = self.state.att_data[frame].get(validator.index) + prop_aggr = self.state.prop_data[frame].get(validator.index) + sync_aggr = self.state.sync_data[frame].get(validator.index) + + if att_aggr is None: + # It's possible that the validator is not assigned to any duty, hence it's performance + # is not presented in the aggregates (e.g. exited, pending for activation etc). + # TODO: check `sync_aggr` to strike (in case of bad sync performance) after validator exit + return + + log_validator = log_operator.validators[validator.index] + + if validator.validator.slashed: + # It means that validator was active during the frame and got slashed and didn't meet the exit + # epoch, so we should not count such validator for operator's share. + log_validator.slashed = True + return + + performance = self._calculate_validator_performance(att_aggr, prop_aggr, sync_aggr) + + if performance > threshold: + # Count of assigned attestations used as a metrics of time + # the validator was active in the current frame. + distribution[validator.lido_id.operatorIndex] += att_aggr.assigned + + log_validator.performance = performance + log_validator.attestations = att_aggr + if prop_aggr: + log_validator.proposals = prop_aggr + if sync_aggr: + log_validator.sync_committee = sync_aggr + + @staticmethod + def _calculate_validator_performance(att_aggr, prop_aggr, sync_aggr) -> float: + performance = att_aggr.perf + if prop_aggr and sync_aggr: + performance = 54 / 64 * att_aggr.perf + 8 / 64 * prop_aggr.perf + 2 / 64 * sync_aggr.perf + elif prop_aggr: + performance = 54 / 62 * att_aggr.perf + 8 / 62 * prop_aggr.perf + elif sync_aggr: + performance = 54 / 56 * att_aggr.perf + 2 / 56 * sync_aggr.perf + if performance > 1: + raise ValueError(f"Invalid performance: {performance=}") + return performance + + def _finalize_distribution( + self, + distribution: dict[NodeOperatorId, int], + blockstamp: ReferenceBlockStamp, + distributed: int, + log: FramePerfLog + ) -> tuple[int, dict[NodeOperatorId, int], FramePerfLog]: + shares: dict[NodeOperatorId, int] = defaultdict(int) + total = sum(distribution.values()) to_distribute = self.w3.csm.fee_distributor.shares_to_distribute(blockstamp.block_hash) - distributed log.distributable = to_distribute @@ -354,7 +388,7 @@ def _calculate_distribution_in_frame( shares[no_id] = to_distribute * no_share // total log.operators[no_id].distributed = shares[no_id] - distributed = sum(s for s in shares.values()) + distributed = sum(shares.values()) if distributed > to_distribute: raise CSMError(f"Invalid distribution: {distributed=} > {to_distribute=}") return distributed, shares, log diff --git a/src/modules/csm/state.py b/src/modules/csm/state.py index a8e5faf4d..99e61469c 100644 --- a/src/modules/csm/state.py +++ b/src/modules/csm/state.py @@ -36,6 +36,16 @@ def add_duty(self, included: bool) -> None: self.included += 1 if included else 0 +def calculate_frames(epochs_to_process: tuple[EpochNumber, ...], epochs_per_frame: int) -> list[Frame]: + """Split epochs to process into frames of `epochs_per_frame` length""" + frames = [] + for frame_epochs in batched(epochs_to_process, epochs_per_frame): + if len(frame_epochs) < epochs_per_frame: + raise ValueError("Insufficient epochs to form a frame") + frames.append((frame_epochs[0], frame_epochs[-1])) + return frames + + class State: """ Processing state of a CSM performance oracle frame. @@ -129,11 +139,10 @@ def clear(self) -> None: assert self.is_empty def find_frame(self, epoch: EpochNumber) -> Frame: - frames = self.calculate_frames(self._epochs_to_process, self._epochs_per_frame) - for epoch_range in frames: + for epoch_range in self.frames: if epoch_range[0] <= epoch <= epoch_range[1]: return epoch_range - raise ValueError(f"Epoch {epoch} is out of frames range: {frames}") + raise ValueError(f"Epoch {epoch} is out of frames range: {self.frames}") def increment_att_duty(self, frame: Frame, val_index: ValidatorIndex, included: bool) -> None: self.att_data[frame][val_index].add_duty(included) @@ -178,16 +187,15 @@ def init_or_migrate( self.commit() def _fill_frames(self, l_epoch: EpochNumber, r_epoch: EpochNumber, epochs_per_frame: int) -> None: - frames = self.calculate_frames(tuple(sequence(l_epoch, r_epoch)), epochs_per_frame) + frames = calculate_frames(tuple(sequence(l_epoch, r_epoch)), epochs_per_frame) for frame in frames: self.att_data.setdefault(frame, defaultdict(DutyAccumulator)) self.prop_data.setdefault(frame, defaultdict(DutyAccumulator)) self.sync_data.setdefault(frame, defaultdict(DutyAccumulator)) def _migrate_or_invalidate(self, l_epoch: EpochNumber, r_epoch: EpochNumber, epochs_per_frame: int) -> bool: - current_frames = self.calculate_frames(self._epochs_to_process, self._epochs_per_frame) - new_frames = self.calculate_frames(tuple(sequence(l_epoch, r_epoch)), epochs_per_frame) - inv_msg = f"Discarding invalid state cache because of frames change. {current_frames=}, {new_frames=}" + new_frames = calculate_frames(tuple(sequence(l_epoch, r_epoch)), epochs_per_frame) + inv_msg = f"Discarding invalid state cache because of frames change. {self.frames=}, {new_frames=}" if self._invalidate_on_epoch_range_change(l_epoch, r_epoch): logger.warning({"msg": inv_msg}) @@ -196,10 +204,10 @@ def _migrate_or_invalidate(self, l_epoch: EpochNumber, r_epoch: EpochNumber, epo frame_expanded = epochs_per_frame > self._epochs_per_frame frame_shrunk = epochs_per_frame < self._epochs_per_frame - has_single_frame = len(current_frames) == len(new_frames) == 1 + has_single_frame = len(self.frames) == len(new_frames) == 1 if has_single_frame and frame_expanded: - current_frame, *_ = current_frames + current_frame, *_ = self.frames new_frame, *_ = new_frames self.att_data[new_frame] = self.att_data.pop(current_frame) self.prop_data[new_frame] = self.prop_data.pop(current_frame) @@ -236,15 +244,9 @@ def validate(self, l_epoch: EpochNumber, r_epoch: EpochNumber) -> None: if epoch not in self._processed_epochs: raise InvalidState(f"Epoch {epoch} missing in processed epochs") - @staticmethod - def calculate_frames(epochs_to_process: tuple[EpochNumber, ...], epochs_per_frame: int) -> list[Frame]: - """Split epochs to process into frames of `epochs_per_frame` length""" - frames = [] - for frame_epochs in batched(epochs_to_process, epochs_per_frame): - if len(frame_epochs) < epochs_per_frame: - raise ValueError("Insufficient epochs to form a frame") - frames.append((frame_epochs[0], frame_epochs[-1])) - return frames + @property + def frames(self) -> list[Frame]: + return calculate_frames(self._epochs_to_process, self._epochs_per_frame) def get_att_network_aggr(self, frame: Frame) -> DutyAccumulator: # TODO: exclude `active_slashed` validators from the calculation diff --git a/src/providers/consensus/client.py b/src/providers/consensus/client.py index 022f4004d..cf24d346c 100644 --- a/src/providers/consensus/client.py +++ b/src/providers/consensus/client.py @@ -133,7 +133,7 @@ def get_block_attestations_and_sync(self, state_id: SlotNumber | BlockRoot) -> t attestations = [BlockAttestationResponse.from_response(**att) for att in data["message"]["body"]["attestations"]] sync = SyncAggregate.from_response(**data["message"]["body"]["sync_aggregate"]) - return attestations, sync + return cast(list[BlockAttestation], attestations), sync @list_of_dataclasses(SlotAttestationCommittee.from_response) def get_attestation_committees( diff --git a/tests/modules/csm/test_state.py b/tests/modules/csm/test_state.py index 0efa4fe96..0f7866b8e 100644 --- a/tests/modules/csm/test_state.py +++ b/tests/modules/csm/test_state.py @@ -3,7 +3,7 @@ import pytest -from src.modules.csm.state import DutyAccumulator, State +from src.modules.csm.state import DutyAccumulator, State, calculate_frames from src.types import EpochNumber, ValidatorIndex from src.utils.range import sequence @@ -209,7 +209,7 @@ def test_new_frame_extends_old_state(self, l_epoch_old, r_epoch_old, l_epoch_new assert state.unprocessed_epochs == set(sequence(l_epoch_new, r_epoch_new)) assert len(state.att_data) == 2 assert list(state.att_data.keys()) == [(l_epoch_old, r_epoch_old), (r_epoch_old + 1, r_epoch_new)] - assert state.calculate_frames(state._epochs_to_process, epochs_per_frame) == [ + assert calculate_frames(state._epochs_to_process, epochs_per_frame) == [ (l_epoch_old, r_epoch_old), (r_epoch_old + 1, r_epoch_new), ] @@ -235,7 +235,7 @@ def test_new_frame_extends_old_state_with_single_frame( assert state.unprocessed_epochs == set(sequence(l_epoch_new, r_epoch_new)) assert len(state.att_data) == 1 assert list(state.att_data.keys())[0] == (l_epoch_new, r_epoch_new) - assert state.calculate_frames(state._epochs_to_process, epochs_per_frame_new) == [(l_epoch_new, r_epoch_new)] + assert calculate_frames(state._epochs_to_process, epochs_per_frame_new) == [(l_epoch_new, r_epoch_new)] @pytest.mark.parametrize( ("old_version", "new_version"), From 76fdc3910777496d373fbf2de4bf08c53efd2148 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Tue, 4 Feb 2025 17:10:36 +0100 Subject: [PATCH 005/162] refactor: calculate_distribution --- src/modules/csm/csm.py | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index 5cd5786d1..e06088421 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -231,26 +231,35 @@ def calculate_distribution( """Computes distribution of fee shares at the given timestamp""" operators_to_validators = self.module_validators_by_node_operators(blockstamp) - distributed = 0 - # Calculate share of each CSM node operator. - shares = defaultdict[NodeOperatorId, int](int) + total_distributed = 0 + total_shares = defaultdict[NodeOperatorId, int](int) logs: list[FramePerfLog] = [] for frame in self.state.frames: from_epoch, to_epoch = frame logger.info({"msg": f"Calculating distribution for frame [{from_epoch};{to_epoch}]"}) + frame_blockstamp = blockstamp if to_epoch != blockstamp.ref_epoch: frame_blockstamp = self._get_ref_blockstamp_for_frame(blockstamp, to_epoch) + + total_to_distribute = self.w3.csm.fee_distributor.shares_to_distribute(blockstamp.block_hash) + to_distribute_in_frame = total_to_distribute - total_distributed + distributed_in_frame, shares_in_frame, log = self._calculate_distribution_in_frame( - frame_blockstamp, operators_to_validators, frame, distributed + frame_blockstamp, operators_to_validators, frame, to_distribute_in_frame ) - distributed += distributed_in_frame + + total_distributed += distributed_in_frame + if total_distributed > total_to_distribute: + raise CSMError(f"Invalid distribution: {total_distributed=} > {total_to_distribute=}") + for no_id, share in shares_in_frame.items(): - shares[no_id] += share + total_shares[no_id] += share + logs.append(log) - return distributed, shares, logs + return total_distributed, total_shares, logs def _get_ref_blockstamp_for_frame( self, blockstamp: ReferenceBlockStamp, frame_ref_epoch: EpochNumber @@ -268,7 +277,7 @@ def _calculate_distribution_in_frame( blockstamp: ReferenceBlockStamp, operators_to_validators: ValidatorsByNodeOperator, frame: Frame, - distributed: int, + to_distribute: int, ): network_perf = self._calculate_network_performance(frame) threshold = self._calculate_threshold(network_perf, blockstamp) @@ -281,7 +290,7 @@ def _calculate_distribution_in_frame( for (_, no_id), validators in operators_to_validators.items(): self._process_operator(validators, no_id, stuck_operators, frame, log, distribution, threshold) - return self._finalize_distribution(distribution, blockstamp, distributed, log) + return self._finalize_distribution(distribution, to_distribute, log) def _calculate_network_performance(self, frame: Frame) -> float: att_perf = self.state.get_att_network_aggr(frame).perf @@ -368,16 +377,14 @@ def _calculate_validator_performance(att_aggr, prop_aggr, sync_aggr) -> float: raise ValueError(f"Invalid performance: {performance=}") return performance + @staticmethod def _finalize_distribution( - self, distribution: dict[NodeOperatorId, int], - blockstamp: ReferenceBlockStamp, - distributed: int, + to_distribute: int, log: FramePerfLog ) -> tuple[int, dict[NodeOperatorId, int], FramePerfLog]: shares: dict[NodeOperatorId, int] = defaultdict(int) total = sum(distribution.values()) - to_distribute = self.w3.csm.fee_distributor.shares_to_distribute(blockstamp.block_hash) - distributed log.distributable = to_distribute if not total: From 1aa722885ffbda44f09d8417864f62a307c27355 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Thu, 13 Feb 2025 12:43:14 +0100 Subject: [PATCH 006/162] refactor: `State` and tests --- src/modules/csm/checkpoint.py | 4 +- src/modules/csm/state.py | 135 ++++---- tests/modules/csm/test_state.py | 592 ++++++++++++++++++++------------ 3 files changed, 445 insertions(+), 286 deletions(-) diff --git a/src/modules/csm/checkpoint.py b/src/modules/csm/checkpoint.py index b111fe197..69d0a79dd 100644 --- a/src/modules/csm/checkpoint.py +++ b/src/modules/csm/checkpoint.py @@ -205,12 +205,12 @@ def _check_duty( for root in block_roots: attestations = self.cc.get_block_attestations(root) process_attestations(attestations, committees, self.eip7549_supported) - + frame = self.state.find_frame(duty_epoch) with lock: for committee in committees.values(): for validator_duty in committee: self.state.increment_duty( - duty_epoch, + frame, validator_duty.index, included=validator_duty.included, ) diff --git a/src/modules/csm/state.py b/src/modules/csm/state.py index fd27a8d62..c269b7fcb 100644 --- a/src/modules/csm/state.py +++ b/src/modules/csm/state.py @@ -13,8 +13,6 @@ logger = logging.getLogger(__name__) -type Frame = tuple[EpochNumber, EpochNumber] - class InvalidState(ValueError): """State has data considered as invalid for a report""" @@ -36,6 +34,10 @@ def add_duty(self, included: bool) -> None: self.included += 1 if included else 0 +type Frame = tuple[EpochNumber, EpochNumber] +type StateData = dict[Frame, defaultdict[ValidatorIndex, AttestationsAccumulator]] + + class State: """ Processing state of a CSM performance oracle frame. @@ -46,7 +48,7 @@ class State: The state can be migrated to be used for another frame's report by calling the `migrate` method. """ - data: dict[Frame, defaultdict[ValidatorIndex, AttestationsAccumulator]] + data: StateData _epochs_to_process: tuple[EpochNumber, ...] _processed_epochs: set[EpochNumber] @@ -54,10 +56,8 @@ class State: _consensus_version: int = 1 - def __init__(self, data: dict[Frame, dict[ValidatorIndex, AttestationsAccumulator]] | None = None) -> None: - self.data = { - frame: defaultdict(AttestationsAccumulator, validators) for frame, validators in (data or {}).items() - } + def __init__(self) -> None: + self.data = {} self._epochs_to_process = tuple() self._processed_epochs = set() self._epochs_per_frame = 0 @@ -110,6 +110,16 @@ def unprocessed_epochs(self) -> set[EpochNumber]: def is_fulfilled(self) -> bool: return not self.unprocessed_epochs + @staticmethod + def calculate_frames(epochs_to_process: tuple[EpochNumber, ...], epochs_per_frame: int) -> list[Frame]: + """Split epochs to process into frames of `epochs_per_frame` length""" + frames = [] + for frame_epochs in batched(epochs_to_process, epochs_per_frame): + if len(frame_epochs) < epochs_per_frame: + raise ValueError("Insufficient epochs to form a frame") + frames.append((frame_epochs[0], frame_epochs[-1])) + return frames + def clear(self) -> None: self.data = {} self._epochs_to_process = tuple() @@ -123,9 +133,10 @@ def find_frame(self, epoch: EpochNumber) -> Frame: return epoch_range raise ValueError(f"Epoch {epoch} is out of frames range: {frames}") - def increment_duty(self, epoch: EpochNumber, val_index: ValidatorIndex, included: bool) -> None: - epoch_range = self.find_frame(epoch) - self.data[epoch_range][val_index].add_duty(included) + def increment_duty(self, frame: Frame, val_index: ValidatorIndex, included: bool) -> None: + if frame not in self.data: + raise ValueError(f"Frame {frame} is not found in the state") + self.data[frame][val_index].add_duty(included) def add_processed_epoch(self, epoch: EpochNumber) -> None: self._processed_epochs.add(epoch) @@ -133,7 +144,9 @@ def add_processed_epoch(self, epoch: EpochNumber) -> None: def log_progress(self) -> None: logger.info({"msg": f"Processed {len(self._processed_epochs)} of {len(self._epochs_to_process)} epochs"}) - def init_or_migrate(self, l_epoch: EpochNumber, r_epoch: EpochNumber, epochs_per_frame: int, consensus_version: int) -> None: + def init_or_migrate( + self, l_epoch: EpochNumber, r_epoch: EpochNumber, epochs_per_frame: int, consensus_version: int + ) -> None: if consensus_version != self._consensus_version: logger.warning( { @@ -143,59 +156,55 @@ def init_or_migrate(self, l_epoch: EpochNumber, r_epoch: EpochNumber, epochs_per ) self.clear() + frames = self.calculate_frames(tuple(sequence(l_epoch, r_epoch)), epochs_per_frame) + frames_data: StateData = {frame: defaultdict(AttestationsAccumulator) for frame in frames} + if not self.is_empty: - invalidated = self._migrate_or_invalidate(l_epoch, r_epoch, epochs_per_frame) - if invalidated: - self.clear() + cached_frames = self.calculate_frames(self._epochs_to_process, self._epochs_per_frame) + if cached_frames == frames: + logger.info({"msg": "No need to migrate duties data cache"}) + return + + frames_data, migration_status = self._migrate_frames_data(cached_frames, frames) + + for current_frame, migrated in migration_status.items(): + if not migrated: + logger.warning({"msg": f"Invalidating frame duties data cache: {current_frame}"}) + for epoch in sequence(*current_frame): + self._processed_epochs.discard(epoch) - self._fill_frames(l_epoch, r_epoch, epochs_per_frame) + self.data = frames_data self._epochs_per_frame = epochs_per_frame self._epochs_to_process = tuple(sequence(l_epoch, r_epoch)) self._consensus_version = consensus_version self.commit() - def _fill_frames(self, l_epoch: EpochNumber, r_epoch: EpochNumber, epochs_per_frame: int) -> None: - frames = self.calculate_frames(tuple(sequence(l_epoch, r_epoch)), epochs_per_frame) - for frame in frames: - self.data.setdefault(frame, defaultdict(AttestationsAccumulator)) - - def _migrate_or_invalidate(self, l_epoch: EpochNumber, r_epoch: EpochNumber, epochs_per_frame: int) -> bool: - current_frames = self.calculate_frames(self._epochs_to_process, self._epochs_per_frame) - new_frames = self.calculate_frames(tuple(sequence(l_epoch, r_epoch)), epochs_per_frame) - inv_msg = f"Discarding invalid state cache because of frames change. {current_frames=}, {new_frames=}" - - if self._invalidate_on_epoch_range_change(l_epoch, r_epoch): - logger.warning({"msg": inv_msg}) - return True - - frame_expanded = epochs_per_frame > self._epochs_per_frame - frame_shrunk = epochs_per_frame < self._epochs_per_frame - - has_single_frame = len(current_frames) == len(new_frames) == 1 - - if has_single_frame and frame_expanded: - current_frame, *_ = current_frames - new_frame, *_ = new_frames - self.data[new_frame] = self.data.pop(current_frame) - logger.info({"msg": f"Migrated state cache to a new frame. {current_frame=}, {new_frame=}"}) - return False - - if has_single_frame and frame_shrunk: - logger.warning({"msg": inv_msg}) - return True - - if not has_single_frame and frame_expanded or frame_shrunk: - logger.warning({"msg": inv_msg}) - return True - - return False - - def _invalidate_on_epoch_range_change(self, l_epoch: EpochNumber, r_epoch: EpochNumber) -> bool: - """Check if the epoch range has been invalidated.""" - for epoch_set in (self._epochs_to_process, self._processed_epochs): - if any(epoch < l_epoch or epoch > r_epoch for epoch in epoch_set): - return True - return False + def _migrate_frames_data( + self, current_frames: list[Frame], new_frames: list[Frame] + ) -> tuple[StateData, dict[Frame, bool]]: + migration_status = {frame: False for frame in current_frames} + new_data: StateData = {frame: defaultdict(AttestationsAccumulator) for frame in new_frames} + + logger.info({"msg": f"Migrating duties data cache: {current_frames=} -> {new_frames=}"}) + + for current_frame in current_frames: + curr_frame_l_epoch, curr_frame_r_epoch = current_frame + for new_frame in new_frames: + if current_frame == new_frame: + new_data[new_frame] = self.data[current_frame] + migration_status[current_frame] = True + break + + new_frame_l_epoch, new_frame_r_epoch = new_frame + if curr_frame_l_epoch >= new_frame_l_epoch and curr_frame_r_epoch <= new_frame_r_epoch: + logger.info({"msg": f"Migrating frame duties data cache: {current_frame=} -> {new_frame=}"}) + for val in self.data[current_frame]: + new_data[new_frame][val].assigned += self.data[current_frame][val].assigned + new_data[new_frame][val].included += self.data[current_frame][val].included + migration_status[current_frame] = True + break + + return new_data, migration_status def validate(self, l_epoch: EpochNumber, r_epoch: EpochNumber) -> None: if not self.is_fulfilled: @@ -209,21 +218,11 @@ def validate(self, l_epoch: EpochNumber, r_epoch: EpochNumber) -> None: if epoch not in self._processed_epochs: raise InvalidState(f"Epoch {epoch} missing in processed epochs") - @staticmethod - def calculate_frames(epochs_to_process: tuple[EpochNumber, ...], epochs_per_frame: int) -> list[Frame]: - """Split epochs to process into frames of `epochs_per_frame` length""" - frames = [] - for frame_epochs in batched(epochs_to_process, epochs_per_frame): - if len(frame_epochs) < epochs_per_frame: - raise ValueError("Insufficient epochs to form a frame") - frames.append((frame_epochs[0], frame_epochs[-1])) - return frames - def get_network_aggr(self, frame: Frame) -> AttestationsAccumulator: # TODO: exclude `active_slashed` validators from the calculation included = assigned = 0 frame_data = self.data.get(frame) - if not frame_data: + if frame_data is None: raise ValueError(f"No data for frame {frame} to calculate network aggregate") for validator, acc in frame_data.items(): if acc.included > acc.assigned: diff --git a/tests/modules/csm/test_state.py b/tests/modules/csm/test_state.py index d781522e2..b5d8f8808 100644 --- a/tests/modules/csm/test_state.py +++ b/tests/modules/csm/test_state.py @@ -1,258 +1,418 @@ +import os +import pickle +from collections import defaultdict from pathlib import Path from unittest.mock import Mock import pytest -from src.modules.csm.state import AttestationsAccumulator, State -from src.types import EpochNumber, ValidatorIndex +from src import variables +from src.modules.csm.state import AttestationsAccumulator, State, InvalidState +from src.types import ValidatorIndex from src.utils.range import sequence -@pytest.fixture() -def state_file_path(tmp_path: Path) -> Path: - return (tmp_path / "mock").with_suffix(State.EXTENSION) +@pytest.fixture(autouse=True) +def remove_state_files(): + state_file = Path("/tmp/state.pkl") + state_buf = Path("/tmp/state.buf") + state_file.unlink(missing_ok=True) + state_buf.unlink(missing_ok=True) + yield + state_file.unlink(missing_ok=True) + state_buf.unlink(missing_ok=True) + + +def test_load_restores_state_from_file(monkeypatch): + monkeypatch.setattr("src.modules.csm.state.State.file", lambda _=None: Path("/tmp/state.pkl")) + state = State() + state.data = { + (0, 31): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}), + } + state.commit() + loaded_state = State.load() + assert loaded_state.data == state.data -@pytest.fixture(autouse=True) -def mock_state_file(state_file_path: Path): - State.file = Mock(return_value=state_file_path) +def test_load_returns_new_instance_if_file_not_found(monkeypatch): + monkeypatch.setattr("src.modules.csm.state.State.file", lambda: Path("/non/existent/path")) + state = State.load() + assert state.is_empty -def test_attestation_aggregate_perf(): - aggr = AttestationsAccumulator(included=333, assigned=777) - assert aggr.perf == pytest.approx(0.4285, abs=1e-4) +def test_load_returns_new_instance_if_empty_object(monkeypatch, tmp_path): + with open('/tmp/state.pkl', "wb") as f: + pickle.dump(None, f) + monkeypatch.setattr("src.modules.csm.state.State.file", lambda: Path("/tmp/state.pkl")) + state = State.load() + assert state.is_empty + + +def test_commit_saves_state_to_file(monkeypatch): + state = State() + state.data = { + (0, 31): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}), + } + monkeypatch.setattr("src.modules.csm.state.State.file", lambda _: Path("/tmp/state.pkl")) + monkeypatch.setattr("os.replace", Mock(side_effect=os.replace)) + state.commit() + with open("/tmp/state.pkl", "rb") as f: + loaded_state = pickle.load(f) + assert loaded_state.data == state.data + os.replace.assert_called_once_with(Path("/tmp/state.buf"), Path("/tmp/state.pkl")) + + +def test_file_returns_correct_path(monkeypatch): + monkeypatch.setattr(variables, "CACHE_PATH", Path("/tmp")) + assert State.file() == Path("/tmp/cache.pkl") + + +def test_buffer_returns_correct_path(monkeypatch): + monkeypatch.setattr(variables, "CACHE_PATH", Path("/tmp")) + state = State() + assert state.buffer == Path("/tmp/cache.buf") + + +def test_is_empty_returns_true_for_empty_state(): + state = State() + assert state.is_empty + + +def test_is_empty_returns_false_for_non_empty_state(): + state = State() + state.data = {(0, 31): defaultdict(AttestationsAccumulator)} + assert not state.is_empty + + +def test_unprocessed_epochs_raises_error_if_epochs_not_set(): + state = State() + with pytest.raises(ValueError, match="Epochs to process are not set"): + state.unprocessed_epochs + + +def test_unprocessed_epochs_returns_correct_set(): + state = State() + state._epochs_to_process = tuple(sequence(0, 95)) + state._processed_epochs = set(sequence(0, 63)) + assert state.unprocessed_epochs == set(sequence(64, 95)) + + +def test_is_fulfilled_returns_true_if_no_unprocessed_epochs(): + state = State() + state._epochs_to_process = tuple(sequence(0, 95)) + state._processed_epochs = set(sequence(0, 95)) + assert state.is_fulfilled + + +def test_is_fulfilled_returns_false_if_unprocessed_epochs_exist(): + state = State() + state._epochs_to_process = tuple(sequence(0, 95)) + state._processed_epochs = set(sequence(0, 63)) + assert not state.is_fulfilled + + +def test_calculate_frames_handles_exact_frame_size(): + epochs = tuple(range(10)) + frames = State.calculate_frames(epochs, 5) + assert frames == [(0, 4), (5, 9)] + + +def test_calculate_frames_raises_error_for_insufficient_epochs(): + epochs = tuple(range(8)) + with pytest.raises(ValueError, match="Insufficient epochs to form a frame"): + State.calculate_frames(epochs, 5) + + +def test_clear_resets_state_to_empty(): + state = State() + state.data = {(0, 31): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)})} + state.clear() + assert state.is_empty + + +def test_find_frame_returns_correct_frame(): + state = State() + state.data = {(0, 31): defaultdict(AttestationsAccumulator)} + assert state.find_frame(15) == (0, 31) -def test_state_avg_perf(): +def test_find_frame_raises_error_for_out_of_range_epoch(): state = State() + state.data = {(0, 31): defaultdict(AttestationsAccumulator)} + with pytest.raises(ValueError, match="Epoch 32 is out of frames range"): + state.find_frame(32) - frame = (0, 999) - with pytest.raises(ValueError): - state.get_network_aggr(frame) +def test_increment_duty_adds_duty_correctly(): + state = State() + frame = (0, 31) + state.data = { + frame: defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}), + } + state.increment_duty(frame, ValidatorIndex(1), True) + assert state.data[frame][ValidatorIndex(1)].assigned == 11 + assert state.data[frame][ValidatorIndex(1)].included == 6 + +def test_increment_duty_creates_new_validator_entry(): state = State() - state.init_or_migrate(*frame, 1000, 1) + frame = (0, 31) state.data = { - frame: { - ValidatorIndex(0): AttestationsAccumulator(included=0, assigned=0), - ValidatorIndex(1): AttestationsAccumulator(included=0, assigned=0), - } + frame: defaultdict(AttestationsAccumulator), } + state.increment_duty(frame, ValidatorIndex(2), True) + assert state.data[frame][ValidatorIndex(2)].assigned == 1 + assert state.data[frame][ValidatorIndex(2)].included == 1 - assert state.get_network_aggr(frame).perf == 0 +def test_increment_duty_handles_non_included_duty(): + state = State() + frame = (0, 31) state.data = { - frame: { - ValidatorIndex(0): AttestationsAccumulator(included=333, assigned=777), - ValidatorIndex(1): AttestationsAccumulator(included=167, assigned=223), - } + frame: defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}), } + state.increment_duty(frame, ValidatorIndex(1), False) + assert state.data[frame][ValidatorIndex(1)].assigned == 11 + assert state.data[frame][ValidatorIndex(1)].included == 5 - assert state.get_network_aggr(frame).perf == 0.5 +def test_increment_duty_raises_error_for_out_of_range_epoch(): + state = State() + state.data = { + (0, 31): defaultdict(AttestationsAccumulator), + } + with pytest.raises(ValueError, match="is not found in the state"): + state.increment_duty((0, 32), ValidatorIndex(1), True) -def test_state_attestations(): - state = State( - { - (0, 999): { - ValidatorIndex(0): AttestationsAccumulator(included=333, assigned=777), - ValidatorIndex(1): AttestationsAccumulator(included=167, assigned=223), - } - } - ) - network_aggr = state.get_network_aggr((0, 999)) +def test_add_processed_epoch_adds_epoch_to_processed_set(): + state = State() + state.add_processed_epoch(5) + assert 5 in state._processed_epochs - assert network_aggr.assigned == 1000 - assert network_aggr.included == 500 +def test_add_processed_epoch_does_not_duplicate_epochs(): + state = State() + state.add_processed_epoch(5) + state.add_processed_epoch(5) + assert len(state._processed_epochs) == 1 -def test_state_load(): - orig = State( - { - (0, 999): { - ValidatorIndex(0): AttestationsAccumulator(included=333, assigned=777), - ValidatorIndex(1): AttestationsAccumulator(included=167, assigned=223), - } - } - ) - orig.commit() - copy = State.load() - assert copy.data == orig.data +def test_init_or_migrate_discards_data_on_version_change(): + state = State() + state._consensus_version = 1 + state.clear = Mock() + state.commit = Mock() + state.init_or_migrate(0, 63, 32, 2) + state.clear.assert_called_once() + state.commit.assert_called_once() -def test_state_clear(): - state = State( - { - (0, 999): { - ValidatorIndex(0): AttestationsAccumulator(included=333, assigned=777), - ValidatorIndex(1): AttestationsAccumulator(included=167, assigned=223), - } - } - ) +def test_init_or_migrate_no_migration_needed(): + state = State() + state._consensus_version = 1 + state._epochs_to_process = tuple(sequence(0, 63)) + state._epochs_per_frame = 32 + state.data = { + (0, 31): defaultdict(AttestationsAccumulator), + (32, 63): defaultdict(AttestationsAccumulator), + } + state.commit = Mock() + state.init_or_migrate(0, 63, 32, 1) + state.commit.assert_not_called() - state._epochs_to_process = (EpochNumber(1), EpochNumber(33)) - state._processed_epochs = {EpochNumber(42), EpochNumber(17)} - state.clear() - assert state.is_empty - assert not state.data +def test_init_or_migrate_migrates_data(): + state = State() + state._consensus_version = 1 + state._epochs_to_process = tuple(sequence(0, 63)) + state._epochs_per_frame = 32 + state.data = { + (0, 31): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}), + (32, 63): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(20, 15)}), + } + state.commit = Mock() + state.init_or_migrate(0, 63, 64, 1) + assert state.data == { + (0, 63): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(30, 20)}), + } + state.commit.assert_called_once() + + +def test_init_or_migrate_invalidates_unmigrated_frames(): + state = State() + state._consensus_version = 1 + state._epochs_to_process = tuple(sequence(0, 63)) + state._epochs_per_frame = 64 + state.data = { + (0, 63): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(30, 20)}), + } + state.commit = Mock() + state.init_or_migrate(0, 31, 32, 1) + assert state.data == { + (0, 31): defaultdict(AttestationsAccumulator), + } + assert state._processed_epochs == set() + state.commit.assert_called_once() + + +def test_init_or_migrate_discards_unmigrated_frame(): + state = State() + state._consensus_version = 1 + state._epochs_to_process = tuple(sequence(0, 95)) + state._epochs_per_frame = 32 + state.data = { + (0, 31): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}), + (32, 63): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(20, 15)}), + (64, 95): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(30, 25)}), + } + state._processed_epochs = set(sequence(0, 95)) + state.commit = Mock() + state.init_or_migrate(0, 63, 32, 1) + assert state.data == { + (0, 31): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}), + (32, 63): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(20, 15)}), + } + assert state._processed_epochs == set(sequence(0, 63)) + state.commit.assert_called_once() + + +def test_migrate_frames_data_creates_new_data_correctly(): + state = State() + current_frames = [(0, 31), (32, 63)] + new_frames = [(0, 63)] + state.data = { + (0, 31): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}), + (32, 63): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(20, 15)}), + } + new_data, migration_status = state._migrate_frames_data(current_frames, new_frames) + assert new_data == { + (0, 63): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(30, 20)}) + } + assert migration_status == {(0, 31): True, (32, 63): True} + + +def test_migrate_frames_data_handles_no_migration(): + state = State() + current_frames = [(0, 31)] + new_frames = [(0, 31)] + state.data = { + (0, 31): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}), + } + new_data, migration_status = state._migrate_frames_data(current_frames, new_frames) + assert new_data == { + (0, 31): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}) + } + assert migration_status == {(0, 31): True} + + +def test_migrate_frames_data_handles_partial_migration(): + state = State() + current_frames = [(0, 31), (32, 63)] + new_frames = [(0, 31), (32, 95)] + state.data = { + (0, 31): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}), + (32, 63): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(20, 15)}), + } + new_data, migration_status = state._migrate_frames_data(current_frames, new_frames) + assert new_data == { + (0, 31): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}), + (32, 95): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(20, 15)}), + } + assert migration_status == {(0, 31): True, (32, 63): True} + + +def test_migrate_frames_data_handles_no_data(): + state = State() + current_frames = [(0, 31)] + new_frames = [(0, 31)] + state.data = {frame: defaultdict(AttestationsAccumulator) for frame in current_frames} + new_data, migration_status = state._migrate_frames_data(current_frames, new_frames) + assert new_data == {(0, 31): defaultdict(AttestationsAccumulator)} + assert migration_status == {(0, 31): True} + + +def test_migrate_frames_data_handles_wider_old_frame(): + state = State() + current_frames = [(0, 63)] + new_frames = [(0, 31), (32, 63)] + state.data = { + (0, 63): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(30, 20)}), + } + new_data, migration_status = state._migrate_frames_data(current_frames, new_frames) + assert new_data == { + (0, 31): defaultdict(AttestationsAccumulator), + (32, 63): defaultdict(AttestationsAccumulator), + } + assert migration_status == {(0, 63): False} + + +def test_validate_raises_error_if_state_not_fulfilled(): + state = State() + state._epochs_to_process = tuple(sequence(0, 95)) + state._processed_epochs = set(sequence(0, 94)) + with pytest.raises(InvalidState, match="State is not fulfilled"): + state.validate(0, 95) + + +def test_validate_raises_error_if_processed_epoch_out_of_range(): + state = State() + state._epochs_to_process = tuple(sequence(0, 95)) + state._processed_epochs = set(sequence(0, 95)) + state._processed_epochs.add(96) + with pytest.raises(InvalidState, match="Processed epoch 96 is out of range"): + state.validate(0, 95) + + +def test_validate_raises_error_if_epoch_missing_in_processed_epochs(): + state = State() + state._epochs_to_process = tuple(sequence(0, 94)) + state._processed_epochs = set(sequence(0, 94)) + with pytest.raises(InvalidState, match="Epoch 95 missing in processed epochs"): + state.validate(0, 95) -def test_state_add_processed_epoch(): +def test_validate_passes_for_fulfilled_state(): state = State() - state.add_processed_epoch(EpochNumber(42)) - state.add_processed_epoch(EpochNumber(17)) - assert state._processed_epochs == {EpochNumber(42), EpochNumber(17)} + state._epochs_to_process = tuple(sequence(0, 95)) + state._processed_epochs = set(sequence(0, 95)) + state.validate(0, 95) -def test_state_inc(): - - frame_0 = (0, 999) - frame_1 = (1000, 1999) - - state = State( - { - frame_0: { - ValidatorIndex(0): AttestationsAccumulator(included=333, assigned=777), - ValidatorIndex(1): AttestationsAccumulator(included=167, assigned=223), - }, - frame_1: { - ValidatorIndex(0): AttestationsAccumulator(included=1, assigned=1), - ValidatorIndex(1): AttestationsAccumulator(included=0, assigned=1), - }, - } - ) - - state.increment_duty(999, ValidatorIndex(0), True) - state.increment_duty(999, ValidatorIndex(0), False) - state.increment_duty(999, ValidatorIndex(1), True) - state.increment_duty(999, ValidatorIndex(1), True) - state.increment_duty(999, ValidatorIndex(1), False) - state.increment_duty(999, ValidatorIndex(2), True) - - state.increment_duty(1000, ValidatorIndex(2), False) - - assert tuple(state.data[frame_0].values()) == ( - AttestationsAccumulator(included=334, assigned=779), - AttestationsAccumulator(included=169, assigned=226), - AttestationsAccumulator(included=1, assigned=1), - ) - - assert tuple(state.data[frame_1].values()) == ( - AttestationsAccumulator(included=1, assigned=1), - AttestationsAccumulator(included=0, assigned=1), - AttestationsAccumulator(included=0, assigned=1), - ) - - -def test_state_file_is_path(): - assert isinstance(State.file(), Path) - - -class TestStateTransition: - """Tests for State's transition for different l_epoch, r_epoch values""" - - @pytest.fixture(autouse=True) - def no_commit(self, monkeypatch: pytest.MonkeyPatch): - monkeypatch.setattr(State, "commit", Mock()) - - def test_empty_to_new_frame(self): - state = State() - assert state.is_empty - - l_epoch = EpochNumber(1) - r_epoch = EpochNumber(255) - - state.init_or_migrate(l_epoch, r_epoch, 255, 1) - - assert not state.is_empty - assert state.unprocessed_epochs == set(sequence(l_epoch, r_epoch)) - - @pytest.mark.parametrize( - ("l_epoch_old", "r_epoch_old", "l_epoch_new", "r_epoch_new"), - [ - pytest.param(1, 255, 256, 510, id="Migrate a..bA..B"), - pytest.param(1, 255, 32, 510, id="Migrate a..A..b..B"), - pytest.param(32, 510, 1, 255, id="Migrate: A..a..B..b"), - ], - ) - def test_new_frame_requires_discarding_state(self, l_epoch_old, r_epoch_old, l_epoch_new, r_epoch_new): - state = State() - state.clear = Mock(side_effect=state.clear) - state.init_or_migrate(l_epoch_old, r_epoch_old, r_epoch_old - l_epoch_old + 1, 1) - state.clear.assert_not_called() - - state.init_or_migrate(l_epoch_new, r_epoch_new, r_epoch_new - l_epoch_new + 1, 1) - state.clear.assert_called_once() - - assert state.unprocessed_epochs == set(sequence(l_epoch_new, r_epoch_new)) - - @pytest.mark.parametrize( - ("l_epoch_old", "r_epoch_old", "l_epoch_new", "r_epoch_new", "epochs_per_frame"), - [ - pytest.param(1, 255, 1, 510, 255, id="Migrate Aa..b..B"), - ], - ) - def test_new_frame_extends_old_state(self, l_epoch_old, r_epoch_old, l_epoch_new, r_epoch_new, epochs_per_frame): - state = State() - state.clear = Mock(side_effect=state.clear) - - state.init_or_migrate(l_epoch_old, r_epoch_old, epochs_per_frame, 1) - state.clear.assert_not_called() - - state.init_or_migrate(l_epoch_new, r_epoch_new, epochs_per_frame, 1) - state.clear.assert_not_called() - - assert state.unprocessed_epochs == set(sequence(l_epoch_new, r_epoch_new)) - assert len(state.data) == 2 - assert list(state.data.keys()) == [(l_epoch_old, r_epoch_old), (r_epoch_old + 1, r_epoch_new)] - assert state.calculate_frames(state._epochs_to_process, epochs_per_frame) == [ - (l_epoch_old, r_epoch_old), - (r_epoch_old + 1, r_epoch_new), - ] - - @pytest.mark.parametrize( - ("l_epoch_old", "r_epoch_old", "epochs_per_frame_old", "l_epoch_new", "r_epoch_new", "epochs_per_frame_new"), - [ - pytest.param(32, 510, 479, 1, 510, 510, id="Migrate: A..a..b..B"), - ], - ) - def test_new_frame_extends_old_state_with_single_frame( - self, l_epoch_old, r_epoch_old, epochs_per_frame_old, l_epoch_new, r_epoch_new, epochs_per_frame_new - ): - state = State() - state.clear = Mock(side_effect=state.clear) - - state.init_or_migrate(l_epoch_old, r_epoch_old, epochs_per_frame_old, 1) - state.clear.assert_not_called() - - state.init_or_migrate(l_epoch_new, r_epoch_new, epochs_per_frame_new, 1) - state.clear.assert_not_called() - - assert state.unprocessed_epochs == set(sequence(l_epoch_new, r_epoch_new)) - assert len(state.data) == 1 - assert list(state.data.keys())[0] == (l_epoch_new, r_epoch_new) - assert state.calculate_frames(state._epochs_to_process, epochs_per_frame_new) == [(l_epoch_new, r_epoch_new)] - - @pytest.mark.parametrize( - ("old_version", "new_version"), - [ - pytest.param(2, 3, id="Increase consensus version"), - pytest.param(3, 2, id="Decrease consensus version"), - ], - ) - def test_consensus_version_change(self, old_version, new_version): - state = State() - state.clear = Mock(side_effect=state.clear) - state._consensus_version = old_version - - l_epoch = r_epoch = EpochNumber(255) - - state.init_or_migrate(l_epoch, r_epoch, 1, old_version) - state.clear.assert_not_called() - - state.init_or_migrate(l_epoch, r_epoch, 1, new_version) - state.clear.assert_called_once() +def test_attestation_aggregate_perf(): + aggr = AttestationsAccumulator(included=333, assigned=777) + assert aggr.perf == pytest.approx(0.4285, abs=1e-4) + + +def test_get_network_aggr_computes_correctly(): + state = State() + state.data = { + (0, 31): defaultdict( + AttestationsAccumulator, + {ValidatorIndex(1): AttestationsAccumulator(10, 5), ValidatorIndex(2): AttestationsAccumulator(20, 15)}, + ) + } + aggr = state.get_network_aggr((0, 31)) + assert aggr.assigned == 30 + assert aggr.included == 20 + + +def test_get_network_aggr_raises_error_for_invalid_accumulator(): + state = State() + state.data = {(0, 31): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 15)})} + with pytest.raises(ValueError, match="Invalid accumulator"): + state.get_network_aggr((0, 31)) + + +def test_get_network_aggr_raises_error_for_missing_frame_data(): + state = State() + with pytest.raises(ValueError, match="No data for frame"): + state.get_network_aggr((0, 31)) + + +def test_get_network_aggr_handles_empty_frame_data(): + state = State() + state.data = {(0, 31): defaultdict(AttestationsAccumulator)} + aggr = state.get_network_aggr((0, 31)) + assert aggr.assigned == 0 + assert aggr.included == 0 From 8163d4d9c102c52ecfadc90c411795fa5e205936 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Thu, 13 Feb 2025 17:37:51 +0100 Subject: [PATCH 007/162] refactor: distribution and tests --- src/modules/csm/csm.py | 173 ++++++---- src/modules/csm/log.py | 3 +- src/modules/csm/state.py | 11 +- .../execution/contracts/cs_fee_distributor.py | 4 +- tests/modules/csm/test_csm_distribution.py | 325 ++++++++++++++++++ tests/modules/csm/test_csm_module.py | 257 -------------- tests/modules/csm/test_state.py | 4 + 7 files changed, 441 insertions(+), 336 deletions(-) create mode 100644 tests/modules/csm/test_csm_distribution.py diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index 3d3619a94..ed146677e 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -12,8 +12,8 @@ ) from src.metrics.prometheus.duration_meter import duration_meter from src.modules.csm.checkpoint import FrameCheckpointProcessor, FrameCheckpointsIterator, MinStepIsNotReached -from src.modules.csm.log import FramePerfLog -from src.modules.csm.state import State, Frame +from src.modules.csm.log import FramePerfLog, OperatorFrameSummary +from src.modules.csm.state import State, Frame, AttestationsAccumulator from src.modules.csm.tree import Tree from src.modules.csm.types import ReportData, Shares from src.modules.submodules.consensus import ConsensusModule @@ -29,13 +29,12 @@ SlotNumber, StakingModuleAddress, StakingModuleId, - ValidatorIndex, ) from src.utils.blockstamp import build_blockstamp from src.utils.cache import global_lru_cache as lru_cache from src.utils.slot import get_next_non_missed_slot, get_reference_blockstamp from src.utils.web3converter import Web3Converter -from src.web3py.extensions.lido_validators import NodeOperatorId, StakingModule, ValidatorsByNodeOperator +from src.web3py.extensions.lido_validators import NodeOperatorId, StakingModule, ValidatorsByNodeOperator, LidoValidator from src.web3py.types import Web3 logger = logging.getLogger(__name__) @@ -102,15 +101,15 @@ def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: if (prev_cid is None) != (prev_root == ZERO_HASH): raise InconsistentData(f"Got inconsistent previous tree data: {prev_root=} {prev_cid=}") - distributed, shares, logs = self.calculate_distribution(blockstamp) + total_distributed, total_rewards, logs = self.calculate_distribution(blockstamp) - if distributed != sum(shares.values()): - raise InconsistentData(f"Invalid distribution: {sum(shares.values())=} != {distributed=}") + if total_distributed != sum(total_rewards.values()): + raise InconsistentData(f"Invalid distribution: {sum(total_rewards.values())=} != {total_distributed=}") log_cid = self.publish_log(logs) - if not distributed and not shares: - logger.info({"msg": "No shares distributed in the current frame"}) + if not total_distributed and not total_rewards: + logger.info({"msg": "No rewards distributed in the current frame"}) return ReportData( self.get_consensus_version(blockstamp), blockstamp.ref_slot, @@ -123,11 +122,11 @@ def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: if prev_cid and prev_root != ZERO_HASH: # Update cumulative amount of shares for all operators. for no_id, acc_shares in self.get_accumulated_shares(prev_cid, prev_root): - shares[no_id] += acc_shares + total_rewards[no_id] += acc_shares else: logger.info({"msg": "No previous distribution. Nothing to accumulate"}) - tree = self.make_tree(shares) + tree = self.make_tree(total_rewards) tree_cid = self.publish_tree(tree) return ReportData( @@ -136,7 +135,7 @@ def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: tree_root=tree.root, tree_cid=tree_cid, log_cid=log_cid, - distributed=distributed, + distributed=total_distributed, ).as_tuple() def is_main_data_submitted(self, blockstamp: BlockStamp) -> bool: @@ -232,26 +231,36 @@ def calculate_distribution( """Computes distribution of fee shares at the given timestamp""" operators_to_validators = self.module_validators_by_node_operators(blockstamp) - distributed = 0 - # Calculate share of each CSM node operator. - shares = defaultdict[NodeOperatorId, int](int) + total_distributed = 0 + total_rewards = defaultdict[NodeOperatorId, int](int) logs: list[FramePerfLog] = [] - for frame in self.state.data: + for frame in self.state.frames: from_epoch, to_epoch = frame logger.info({"msg": f"Calculating distribution for frame [{from_epoch};{to_epoch}]"}) + frame_blockstamp = blockstamp if to_epoch != blockstamp.ref_epoch: frame_blockstamp = self._get_ref_blockstamp_for_frame(blockstamp, to_epoch) - distributed_in_frame, shares_in_frame, log = self._calculate_distribution_in_frame( - frame_blockstamp, operators_to_validators, frame, distributed + + total_rewards_to_distribute = self.w3.csm.fee_distributor.shares_to_distribute(frame_blockstamp.block_hash) + rewards_to_distribute_in_frame = total_rewards_to_distribute - total_distributed + + rewards_in_frame, log = self._calculate_distribution_in_frame( + frame, frame_blockstamp, rewards_to_distribute_in_frame, operators_to_validators ) - distributed += distributed_in_frame - for no_id, share in shares_in_frame.items(): - shares[no_id] += share + distributed_in_frame = sum(rewards_in_frame.values()) + + total_distributed += distributed_in_frame + if total_distributed > total_rewards_to_distribute: + raise CSMError(f"Invalid distribution: {total_distributed=} > {total_rewards_to_distribute=}") + + for no_id, rewards in rewards_in_frame.items(): + total_rewards[no_id] += rewards + logs.append(log) - return distributed, shares, logs + return total_distributed, total_rewards, logs def _get_ref_blockstamp_for_frame( self, blockstamp: ReferenceBlockStamp, frame_ref_epoch: EpochNumber @@ -266,63 +275,85 @@ def _get_ref_blockstamp_for_frame( def _calculate_distribution_in_frame( self, - blockstamp: ReferenceBlockStamp, - operators_to_validators: ValidatorsByNodeOperator, frame: Frame, - distributed: int, + blockstamp: ReferenceBlockStamp, + rewards_to_distribute: int, + operators_to_validators: ValidatorsByNodeOperator ): - network_perf = self.state.get_network_aggr(frame).perf - threshold = network_perf - self.w3.csm.oracle.perf_leeway_bp(blockstamp.block_hash) / TOTAL_BASIS_POINTS - - # Build the map of the current distribution operators. - distribution: dict[NodeOperatorId, int] = defaultdict(int) - stuck_operators = self.stuck_operators(blockstamp) + threshold = self._get_performance_threshold(frame, blockstamp) log = FramePerfLog(blockstamp, frame, threshold) + participation_shares: defaultdict[NodeOperatorId, int] = defaultdict(int) + + stuck_operators = self.stuck_operators(blockstamp) for (_, no_id), validators in operators_to_validators.items(): + log_operator = log.operators[no_id] if no_id in stuck_operators: - log.operators[no_id].stuck = True + log_operator.stuck = True + continue + for validator in validators: + duty = self.state.data[frame].get(validator.index) + self.process_validator_duty(validator, duty, threshold, participation_shares, log_operator) + + rewards_distribution = self.calc_rewards_distribution_in_frame(participation_shares, rewards_to_distribute) + + for no_id, no_rewards in rewards_distribution.items(): + log.operators[no_id].distributed = no_rewards + + log.distributable = rewards_to_distribute + + return rewards_distribution, log + + def _get_performance_threshold(self, frame: Frame, blockstamp: ReferenceBlockStamp) -> float: + network_perf = self.state.get_network_aggr(frame).perf + perf_leeway = self.w3.csm.oracle.perf_leeway_bp(blockstamp.block_hash) / TOTAL_BASIS_POINTS + threshold = network_perf - perf_leeway + return threshold + + @staticmethod + def process_validator_duty( + validator: LidoValidator, + attestation_duty: AttestationsAccumulator | None, + threshold: float, + participation_shares: defaultdict[NodeOperatorId, int], + log_operator: OperatorFrameSummary + ): + if attestation_duty is None: + # It's possible that the validator is not assigned to any duty, hence it's performance + # is not presented in the aggregates (e.g. exited, pending for activation etc). + # TODO: check `sync_aggr` to strike (in case of bad sync performance) after validator exit + return + + log_validator = log_operator.validators[validator.index] + + if validator.validator.slashed is True: + # It means that validator was active during the frame and got slashed and didn't meet the exit + # epoch, so we should not count such validator for operator's share. + log_validator.slashed = True + return + + if attestation_duty.perf > threshold: + # Count of assigned attestations used as a metrics of time + # the validator was active in the current frame. + participation_shares[validator.lido_id.operatorIndex] += attestation_duty.assigned + + log_validator.attestation_duty = attestation_duty + + @staticmethod + def calc_rewards_distribution_in_frame( + participation_shares: dict[NodeOperatorId, int], + rewards_to_distribute: int, + ) -> dict[NodeOperatorId, int]: + rewards_distribution: dict[NodeOperatorId, int] = defaultdict(int) + total_participation = sum(participation_shares.values()) + + for no_id, no_participation_share in participation_shares.items(): + if no_participation_share == 0: + # Skip operators with zero participation continue + rewards_distribution[no_id] = rewards_to_distribute * no_participation_share // total_participation - for v in validators: - aggr = self.state.data[frame].get(ValidatorIndex(int(v.index))) - - if aggr is None: - # It's possible that the validator is not assigned to any duty, hence it's performance - # is not presented in the aggregates (e.g. exited, pending for activation etc). - continue - - if v.validator.slashed is True: - # It means that validator was active during the frame and got slashed and didn't meet the exit - # epoch, so we should not count such validator for operator's share. - log.operators[no_id].validators[v.index].slashed = True - continue - - if aggr.perf > threshold: - # Count of assigned attestations used as a metrics of time - # the validator was active in the current frame. - distribution[no_id] += aggr.assigned - - log.operators[no_id].validators[v.index].perf = aggr - - # Calculate share of each CSM node operator. - shares = defaultdict[NodeOperatorId, int](int) - total = sum(p for p in distribution.values()) - to_distribute = self.w3.csm.fee_distributor.shares_to_distribute(blockstamp.block_hash) - distributed - log.distributable = to_distribute - - if not total: - return 0, shares, log - - for no_id, no_share in distribution.items(): - if no_share: - shares[no_id] = to_distribute * no_share // total - log.operators[no_id].distributed = shares[no_id] - - distributed = sum(s for s in shares.values()) - if distributed > to_distribute: - raise CSMError(f"Invalid distribution: {distributed=} > {to_distribute=}") - return distributed, shares, log + return rewards_distribution def get_accumulated_shares(self, cid: CID, root: HexBytes) -> Iterator[tuple[NodeOperatorId, Shares]]: logger.info({"msg": "Fetching tree by CID from IPFS", "cid": repr(cid)}) diff --git a/src/modules/csm/log.py b/src/modules/csm/log.py index 39832c8c0..29ab24902 100644 --- a/src/modules/csm/log.py +++ b/src/modules/csm/log.py @@ -12,8 +12,7 @@ class LogJSONEncoder(json.JSONEncoder): ... @dataclass class ValidatorFrameSummary: - # TODO: Should be renamed. Perf means different things in different contexts - perf: AttestationsAccumulator = field(default_factory=AttestationsAccumulator) + attestation_duty: AttestationsAccumulator = field(default_factory=AttestationsAccumulator) slashed: bool = False diff --git a/src/modules/csm/state.py b/src/modules/csm/state.py index c269b7fcb..e6fb7d866 100644 --- a/src/modules/csm/state.py +++ b/src/modules/csm/state.py @@ -110,6 +110,10 @@ def unprocessed_epochs(self) -> set[EpochNumber]: def is_fulfilled(self) -> bool: return not self.unprocessed_epochs + @property + def frames(self): + return self.calculate_frames(self._epochs_to_process, self._epochs_per_frame) + @staticmethod def calculate_frames(epochs_to_process: tuple[EpochNumber, ...], epochs_per_frame: int) -> list[Frame]: """Split epochs to process into frames of `epochs_per_frame` length""" @@ -127,11 +131,10 @@ def clear(self) -> None: assert self.is_empty def find_frame(self, epoch: EpochNumber) -> Frame: - frames = self.data.keys() - for epoch_range in frames: + for epoch_range in self.frames: if epoch_range[0] <= epoch <= epoch_range[1]: return epoch_range - raise ValueError(f"Epoch {epoch} is out of frames range: {frames}") + raise ValueError(f"Epoch {epoch} is out of frames range: {self.frames}") def increment_duty(self, frame: Frame, val_index: ValidatorIndex, included: bool) -> None: if frame not in self.data: @@ -160,7 +163,7 @@ def init_or_migrate( frames_data: StateData = {frame: defaultdict(AttestationsAccumulator) for frame in frames} if not self.is_empty: - cached_frames = self.calculate_frames(self._epochs_to_process, self._epochs_per_frame) + cached_frames = self.frames if cached_frames == frames: logger.info({"msg": "No need to migrate duties data cache"}) return diff --git a/src/providers/execution/contracts/cs_fee_distributor.py b/src/providers/execution/contracts/cs_fee_distributor.py index 937c7dc49..8a556b250 100644 --- a/src/providers/execution/contracts/cs_fee_distributor.py +++ b/src/providers/execution/contracts/cs_fee_distributor.py @@ -3,7 +3,7 @@ from eth_typing import ChecksumAddress from hexbytes import HexBytes from web3 import Web3 -from web3.types import BlockIdentifier +from web3.types import BlockIdentifier, Wei from ..base_interface import ContractInterface @@ -26,7 +26,7 @@ def oracle(self, block_identifier: BlockIdentifier = "latest") -> ChecksumAddres ) return Web3.to_checksum_address(resp) - def shares_to_distribute(self, block_identifier: BlockIdentifier = "latest") -> int: + def shares_to_distribute(self, block_identifier: BlockIdentifier = "latest") -> Wei: """Returns the amount of shares that are pending to be distributed""" resp = self.functions.pendingSharesToDistribute().call(block_identifier=block_identifier) diff --git a/tests/modules/csm/test_csm_distribution.py b/tests/modules/csm/test_csm_distribution.py new file mode 100644 index 000000000..cb07c2e95 --- /dev/null +++ b/tests/modules/csm/test_csm_distribution.py @@ -0,0 +1,325 @@ +from collections import defaultdict +from unittest.mock import Mock + +import pytest +from web3.types import Wei + +from src.constants import UINT64_MAX +from src.modules.csm.csm import CSOracle, CSMError +from src.modules.csm.log import ValidatorFrameSummary, OperatorFrameSummary +from src.modules.csm.state import AttestationsAccumulator, State +from src.types import NodeOperatorId, ValidatorIndex +from src.web3py.extensions import CSM +from tests.factory.no_registry import LidoValidatorFactory + + +@pytest.fixture(autouse=True) +def mock_get_module_id(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(CSOracle, "_get_module_id", Mock()) + + +@pytest.fixture() +def module(web3, csm: CSM): + yield CSOracle(web3) + + +def test_calculate_distribution_handles_single_frame(module): + module.state = Mock() + module.state.frames = [(1, 2)] + blockstamp = Mock() + module.module_validators_by_node_operators = Mock() + module._get_ref_blockstamp_for_frame = Mock(return_value=blockstamp) + module.w3.csm.fee_distributor.shares_to_distribute = Mock(return_value=500) + module._calculate_distribution_in_frame = Mock(return_value=({NodeOperatorId(1): 500}, Mock())) + + total_distributed, total_rewards, logs = module.calculate_distribution(blockstamp) + + assert total_distributed == 500 + assert total_rewards[NodeOperatorId(1)] == 500 + assert len(logs) == 1 + + +def test_calculate_distribution_handles_multiple_frames(module): + module.state = Mock() + module.state.frames = [(1, 2), (3, 4), (5, 6)] + blockstamp = Mock() + module.module_validators_by_node_operators = Mock() + module._get_ref_blockstamp_for_frame = Mock(return_value=blockstamp) + module.w3.csm.fee_distributor.shares_to_distribute = Mock(return_value=800) + module._calculate_distribution_in_frame = Mock( + side_effect=[ + ({NodeOperatorId(1): 500}, Mock()), + ({NodeOperatorId(1): 136}, Mock()), + ({NodeOperatorId(1): 164}, Mock()), + ] + ) + + total_distributed, total_rewards, logs = module.calculate_distribution(blockstamp) + + assert total_distributed == 800 + assert total_rewards[NodeOperatorId(1)] == 800 + assert len(logs) == 3 + + +def test_calculate_distribution_handles_invalid_distribution(module): + module.state = Mock() + module.state.frames = [(1, 2)] + blockstamp = Mock() + module.module_validators_by_node_operators = Mock() + module._get_ref_blockstamp_for_frame = Mock(return_value=blockstamp) + module.w3.csm.fee_distributor.shares_to_distribute = Mock(return_value=500) + module._calculate_distribution_in_frame = Mock(return_value=({NodeOperatorId(1): 600}, Mock())) + + with pytest.raises(CSMError, match="Invalid distribution"): + module.calculate_distribution(blockstamp) + + +def test_calculate_distribution_in_frame_handles_stuck_operator(module): + frame = Mock() + blockstamp = Mock() + rewards_to_distribute = UINT64_MAX + operators_to_validators = {(Mock(), NodeOperatorId(1)): [LidoValidatorFactory.build()]} + module.state = State() + module.state.data = {frame: defaultdict(AttestationsAccumulator)} + module.stuck_operators = Mock(return_value={NodeOperatorId(1)}) + module._get_performance_threshold = Mock() + + rewards_distribution, log = module._calculate_distribution_in_frame( + frame, blockstamp, rewards_to_distribute, operators_to_validators + ) + + assert rewards_distribution[NodeOperatorId(1)] == 0 + assert log.operators[NodeOperatorId(1)].stuck is True + assert log.operators[NodeOperatorId(1)].distributed == 0 + assert log.operators[NodeOperatorId(1)].validators == defaultdict(ValidatorFrameSummary) + + +def test_calculate_distribution_in_frame_handles_no_attestation_duty(module): + frame = Mock() + blockstamp = Mock() + rewards_to_distribute = UINT64_MAX + validator = LidoValidatorFactory.build() + node_operator_id = validator.lido_id.operatorIndex + operators_to_validators = {(Mock(), node_operator_id): [validator]} + module.state = State() + module.state.data = {frame: defaultdict(AttestationsAccumulator)} + module.stuck_operators = Mock(return_value=set()) + module._get_performance_threshold = Mock() + + rewards_distribution, log = module._calculate_distribution_in_frame( + frame, blockstamp, rewards_to_distribute, operators_to_validators + ) + + assert rewards_distribution[node_operator_id] == 0 + assert log.operators[node_operator_id].stuck is False + assert log.operators[node_operator_id].distributed == 0 + assert log.operators[node_operator_id].validators == defaultdict(ValidatorFrameSummary) + + +def test_calculate_distribution_in_frame_handles_above_threshold_performance(module): + frame = Mock() + blockstamp = Mock() + rewards_to_distribute = UINT64_MAX + validator = LidoValidatorFactory.build() + validator.validator.slashed = False + node_operator_id = validator.lido_id.operatorIndex + operators_to_validators = {(Mock(), node_operator_id): [validator]} + module.state = State() + attestation_duty = AttestationsAccumulator(assigned=10, included=6) + module.state.data = {frame: {validator.index: attestation_duty}} + module.stuck_operators = Mock(return_value=set()) + module._get_performance_threshold = Mock(return_value=0.5) + + rewards_distribution, log = module._calculate_distribution_in_frame( + frame, blockstamp, rewards_to_distribute, operators_to_validators + ) + + assert rewards_distribution[node_operator_id] > 0 # no need to check exact value + assert log.operators[node_operator_id].stuck is False + assert log.operators[node_operator_id].distributed > 0 + assert log.operators[node_operator_id].validators[validator.index].attestation_duty == attestation_duty + + +def test_calculate_distribution_in_frame_handles_below_threshold_performance(module): + frame = Mock() + blockstamp = Mock() + rewards_to_distribute = UINT64_MAX + validator = LidoValidatorFactory.build() + validator.validator.slashed = False + node_operator_id = validator.lido_id.operatorIndex + operators_to_validators = {(Mock(), node_operator_id): [validator]} + module.state = State() + attestation_duty = AttestationsAccumulator(assigned=10, included=5) + module.state.data = {frame: {validator.index: attestation_duty}} + module.stuck_operators = Mock(return_value=set()) + module._get_performance_threshold = Mock(return_value=0.5) + + rewards_distribution, log = module._calculate_distribution_in_frame( + frame, blockstamp, rewards_to_distribute, operators_to_validators + ) + + assert rewards_distribution[node_operator_id] == 0 + assert log.operators[node_operator_id].stuck is False + assert log.operators[node_operator_id].distributed == 0 + assert log.operators[node_operator_id].validators[validator.index].attestation_duty == attestation_duty + + +def test_performance_threshold_calculates_correctly(module): + state = State() + state.data = { + (0, 31): { + ValidatorIndex(1): AttestationsAccumulator(10, 10), + ValidatorIndex(2): AttestationsAccumulator(10, 10), + }, + } + module.w3.csm.oracle.perf_leeway_bp.return_value = 500 + module.state = state + + threshold = module._get_performance_threshold((0, 31), Mock()) + + assert threshold == 0.95 + + +def test_performance_threshold_handles_zero_leeway(module): + state = State() + state.data = { + (0, 31): { + ValidatorIndex(1): AttestationsAccumulator(10, 10), + ValidatorIndex(2): AttestationsAccumulator(10, 10), + }, + } + module.w3.csm.oracle.perf_leeway_bp.return_value = 0 + module.state = state + + threshold = module._get_performance_threshold((0, 31), Mock()) + + assert threshold == 1.0 + + +def test_performance_threshold_handles_high_leeway(module): + state = State() + state.data = { + (0, 31): {ValidatorIndex(1): AttestationsAccumulator(10, 1), ValidatorIndex(2): AttestationsAccumulator(10, 1)}, + } + module.w3.csm.oracle.perf_leeway_bp.return_value = 5000 + module.state = state + + threshold = module._get_performance_threshold((0, 31), Mock()) + + assert threshold == -0.4 + + +def test_process_validator_duty_handles_above_threshold_performance(): + validator = LidoValidatorFactory.build() + validator.validator.slashed = False + log_operator = Mock() + log_operator.validators = defaultdict(ValidatorFrameSummary) + participation_shares = defaultdict(int) + threshold = 0.5 + + attestation_duty = AttestationsAccumulator(assigned=10, included=6) + + CSOracle.process_validator_duty(validator, attestation_duty, threshold, participation_shares, log_operator) + + assert participation_shares[validator.lido_id.operatorIndex] == 10 + assert log_operator.validators[validator.index].attestation_duty == attestation_duty + + +def test_process_validator_duty_handles_below_threshold_performance(): + validator = LidoValidatorFactory.build() + validator.validator.slashed = False + log_operator = Mock() + log_operator.validators = defaultdict(ValidatorFrameSummary) + participation_shares = defaultdict(int) + threshold = 0.5 + + attestation_duty = AttestationsAccumulator(assigned=10, included=4) + + CSOracle.process_validator_duty(validator, attestation_duty, threshold, participation_shares, log_operator) + + assert participation_shares[validator.lido_id.operatorIndex] == 0 + assert log_operator.validators[validator.index].attestation_duty == attestation_duty + + +def test_process_validator_duty_handles_non_empy_participation_shares(): + validator = LidoValidatorFactory.build() + validator.validator.slashed = False + log_operator = Mock() + log_operator.validators = defaultdict(ValidatorFrameSummary) + participation_shares = {validator.lido_id.operatorIndex: 25} + threshold = 0.5 + + attestation_duty = AttestationsAccumulator(assigned=10, included=6) + + CSOracle.process_validator_duty(validator, attestation_duty, threshold, participation_shares, log_operator) + + assert participation_shares[validator.lido_id.operatorIndex] == 35 + assert log_operator.validators[validator.index].attestation_duty == attestation_duty + + +def test_process_validator_duty_handles_no_duty_assigned(): + validator = LidoValidatorFactory.build() + log_operator = Mock() + log_operator.validators = defaultdict(ValidatorFrameSummary) + participation_shares = defaultdict(int) + threshold = 0.5 + + CSOracle.process_validator_duty(validator, None, threshold, participation_shares, log_operator) + + assert participation_shares[validator.lido_id.operatorIndex] == 0 + assert validator.index not in log_operator.validators + + +def test_process_validator_duty_handles_slashed_validator(): + validator = LidoValidatorFactory.build() + validator.validator.slashed = True + log_operator = Mock() + log_operator.validators = defaultdict(ValidatorFrameSummary) + participation_shares = defaultdict(int) + threshold = 0.5 + + attestation_duty = AttestationsAccumulator(assigned=1, included=1) + + CSOracle.process_validator_duty(validator, attestation_duty, threshold, participation_shares, log_operator) + + assert participation_shares[validator.lido_id.operatorIndex] == 0 + assert log_operator.validators[validator.index].slashed is True + + +def test_calc_rewards_distribution_in_frame_correctly_distributes_rewards(): + participation_shares = {NodeOperatorId(1): 100, NodeOperatorId(2): 200} + rewards_to_distribute = Wei(1 * 10**18) + + rewards_distribution = CSOracle.calc_rewards_distribution_in_frame(participation_shares, rewards_to_distribute) + + assert rewards_distribution[NodeOperatorId(1)] == Wei(333333333333333333) + assert rewards_distribution[NodeOperatorId(2)] == Wei(666666666666666666) + + +def test_calc_rewards_distribution_in_frame_handles_zero_participation(): + participation_shares = {NodeOperatorId(1): 0, NodeOperatorId(2): 0} + rewards_to_distribute = Wei(1 * 10**18) + + rewards_distribution = CSOracle.calc_rewards_distribution_in_frame(participation_shares, rewards_to_distribute) + + assert rewards_distribution[NodeOperatorId(1)] == 0 + assert rewards_distribution[NodeOperatorId(2)] == 0 + + +def test_calc_rewards_distribution_in_frame_handles_no_participation(): + participation_shares = {} + rewards_to_distribute = Wei(1 * 10**18) + + rewards_distribution = CSOracle.calc_rewards_distribution_in_frame(participation_shares, rewards_to_distribute) + + assert len(rewards_distribution) == 0 + + +def test_calc_rewards_distribution_in_frame_handles_partial_participation(): + participation_shares = {NodeOperatorId(1): 100, NodeOperatorId(2): 0} + rewards_to_distribute = Wei(1 * 10**18) + + rewards_distribution = CSOracle.calc_rewards_distribution_in_frame(participation_shares, rewards_to_distribute) + + assert rewards_distribution[NodeOperatorId(1)] == Wei(1 * 10**18) + assert rewards_distribution[NodeOperatorId(2)] == 0 diff --git a/tests/modules/csm/test_csm_module.py b/tests/modules/csm/test_csm_module.py index cdb0c92c5..1d396c083 100644 --- a/tests/modules/csm/test_csm_module.py +++ b/tests/modules/csm/test_csm_module.py @@ -132,263 +132,6 @@ def test_stuck_operators_left_border_before_enact(module: CSOracle, csm: CSM, ca assert caplog.messages[0].startswith("No CSM digest at blockstamp") -def test_calculate_distribution(module: CSOracle, csm: CSM): - csm.fee_distributor.shares_to_distribute = Mock(return_value=10_000) - csm.oracle.perf_leeway_bp = Mock(return_value=500) - - module.module_validators_by_node_operators = Mock( - return_value={ - (None, NodeOperatorId(0)): [Mock(index=0, validator=Mock(slashed=False))], - (None, NodeOperatorId(1)): [Mock(index=1, validator=Mock(slashed=False))], - (None, NodeOperatorId(2)): [Mock(index=2, validator=Mock(slashed=False))], # stuck - (None, NodeOperatorId(3)): [Mock(index=3, validator=Mock(slashed=False))], - (None, NodeOperatorId(4)): [Mock(index=4, validator=Mock(slashed=False))], # stuck - (None, NodeOperatorId(5)): [ - Mock(index=5, validator=Mock(slashed=False)), - Mock(index=6, validator=Mock(slashed=False)), - ], - (None, NodeOperatorId(6)): [ - Mock(index=7, validator=Mock(slashed=False)), - Mock(index=8, validator=Mock(slashed=False)), - ], - (None, NodeOperatorId(7)): [Mock(index=9, validator=Mock(slashed=False))], - (None, NodeOperatorId(8)): [ - Mock(index=10, validator=Mock(slashed=False)), - Mock(index=11, validator=Mock(slashed=True)), - ], - (None, NodeOperatorId(9)): [Mock(index=12, validator=Mock(slashed=True))], - } - ) - module.stuck_operators = Mock( - return_value=[ - NodeOperatorId(2), - NodeOperatorId(4), - ] - ) - - frame_0: Frame = (EpochNumber(0), EpochNumber(999)) - - module.state.init_or_migrate(*frame_0, epochs_per_frame=1000, consensus_version=1) - module.state = State( - { - frame_0: { - ValidatorIndex(0): AttestationsAccumulator(included=200, assigned=200), # short on frame - ValidatorIndex(1): AttestationsAccumulator(included=1000, assigned=1000), - ValidatorIndex(2): AttestationsAccumulator(included=1000, assigned=1000), - ValidatorIndex(3): AttestationsAccumulator(included=999, assigned=1000), - ValidatorIndex(4): AttestationsAccumulator(included=900, assigned=1000), - ValidatorIndex(5): AttestationsAccumulator(included=500, assigned=1000), # underperforming - ValidatorIndex(6): AttestationsAccumulator(included=0, assigned=0), # underperforming - ValidatorIndex(7): AttestationsAccumulator(included=900, assigned=1000), - ValidatorIndex(8): AttestationsAccumulator(included=500, assigned=1000), # underperforming - # ValidatorIndex(9): AttestationsAggregate(included=0, assigned=0), # missing in state - ValidatorIndex(10): AttestationsAccumulator(included=1000, assigned=1000), - ValidatorIndex(11): AttestationsAccumulator(included=1000, assigned=1000), - ValidatorIndex(12): AttestationsAccumulator(included=1000, assigned=1000), - } - } - ) - - l_epoch, r_epoch = frame_0 - - frame_0_network_aggr = module.state.get_network_aggr(frame_0) - - blockstamp = ReferenceBlockStampFactory.build(slot_number=r_epoch * 32, ref_epoch=r_epoch, ref_slot=r_epoch * 32) - _, shares, logs = module.calculate_distribution(blockstamp=blockstamp) - - log, *_ = logs - - assert tuple(shares.items()) == ( - (NodeOperatorId(0), 476), - (NodeOperatorId(1), 2380), - (NodeOperatorId(3), 2380), - (NodeOperatorId(6), 2380), - (NodeOperatorId(8), 2380), - ) - - assert tuple(log.operators.keys()) == ( - NodeOperatorId(0), - NodeOperatorId(1), - NodeOperatorId(2), - NodeOperatorId(3), - NodeOperatorId(4), - NodeOperatorId(5), - NodeOperatorId(6), - # NodeOperatorId(7), # Missing in state - NodeOperatorId(8), - NodeOperatorId(9), - ) - - assert not log.operators[NodeOperatorId(1)].stuck - - assert log.operators[NodeOperatorId(2)].validators == {} - assert log.operators[NodeOperatorId(2)].stuck - assert log.operators[NodeOperatorId(4)].validators == {} - assert log.operators[NodeOperatorId(4)].stuck - - assert 5 in log.operators[NodeOperatorId(5)].validators - assert 6 in log.operators[NodeOperatorId(5)].validators - assert 7 in log.operators[NodeOperatorId(6)].validators - - assert log.operators[NodeOperatorId(0)].distributed == 476 - assert log.operators[NodeOperatorId(1)].distributed == 2380 - assert log.operators[NodeOperatorId(2)].distributed == 0 - assert log.operators[NodeOperatorId(3)].distributed == 2380 - assert log.operators[NodeOperatorId(6)].distributed == 2380 - - assert log.frame == frame_0 - assert log.threshold == frame_0_network_aggr.perf - 0.05 - - -def test_calculate_distribution_with_missed_with_two_frames(module: CSOracle, csm: CSM): - csm.oracle.perf_leeway_bp = Mock(return_value=500) - csm.fee_distributor.shares_to_distribute = Mock(side_effect=[10000, 20000]) - - module.module_validators_by_node_operators = Mock( - return_value={ - (None, NodeOperatorId(0)): [Mock(index=0, validator=Mock(slashed=False))], - (None, NodeOperatorId(1)): [Mock(index=1, validator=Mock(slashed=False))], - (None, NodeOperatorId(2)): [Mock(index=2, validator=Mock(slashed=False))], # stuck - (None, NodeOperatorId(3)): [Mock(index=3, validator=Mock(slashed=False))], - (None, NodeOperatorId(4)): [Mock(index=4, validator=Mock(slashed=False))], # stuck - (None, NodeOperatorId(5)): [ - Mock(index=5, validator=Mock(slashed=False)), - Mock(index=6, validator=Mock(slashed=False)), - ], - (None, NodeOperatorId(6)): [ - Mock(index=7, validator=Mock(slashed=False)), - Mock(index=8, validator=Mock(slashed=False)), - ], - (None, NodeOperatorId(7)): [Mock(index=9, validator=Mock(slashed=False))], - (None, NodeOperatorId(8)): [ - Mock(index=10, validator=Mock(slashed=False)), - Mock(index=11, validator=Mock(slashed=True)), - ], - (None, NodeOperatorId(9)): [Mock(index=12, validator=Mock(slashed=True))], - } - ) - - module.stuck_operators = Mock( - side_effect=[ - [ - NodeOperatorId(2), - NodeOperatorId(4), - ], - [ - NodeOperatorId(2), - NodeOperatorId(4), - ], - ] - ) - - module.state = State() - l_epoch, r_epoch = EpochNumber(0), EpochNumber(1999) - frame_0 = (0, 999) - frame_1 = (1000, 1999) - module.state.init_or_migrate(l_epoch, r_epoch, epochs_per_frame=1000, consensus_version=1) - module.state = State( - { - frame_0: { - ValidatorIndex(0): AttestationsAccumulator(included=200, assigned=200), # short on frame - ValidatorIndex(1): AttestationsAccumulator(included=1000, assigned=1000), - ValidatorIndex(2): AttestationsAccumulator(included=1000, assigned=1000), - ValidatorIndex(3): AttestationsAccumulator(included=999, assigned=1000), - ValidatorIndex(4): AttestationsAccumulator(included=900, assigned=1000), - ValidatorIndex(5): AttestationsAccumulator(included=500, assigned=1000), # underperforming - ValidatorIndex(6): AttestationsAccumulator(included=0, assigned=0), # underperforming - ValidatorIndex(7): AttestationsAccumulator(included=900, assigned=1000), - ValidatorIndex(8): AttestationsAccumulator(included=500, assigned=1000), # underperforming - # ValidatorIndex(9): AttestationsAggregate(included=0, assigned=0), # missing in state - ValidatorIndex(10): AttestationsAccumulator(included=1000, assigned=1000), - ValidatorIndex(11): AttestationsAccumulator(included=1000, assigned=1000), - ValidatorIndex(12): AttestationsAccumulator(included=1000, assigned=1000), - }, - frame_1: { - ValidatorIndex(0): AttestationsAccumulator(included=200, assigned=200), # short on frame - ValidatorIndex(1): AttestationsAccumulator(included=1000, assigned=1000), - ValidatorIndex(2): AttestationsAccumulator(included=1000, assigned=1000), - ValidatorIndex(3): AttestationsAccumulator(included=999, assigned=1000), - ValidatorIndex(4): AttestationsAccumulator(included=900, assigned=1000), - ValidatorIndex(5): AttestationsAccumulator(included=500, assigned=1000), # underperforming - ValidatorIndex(6): AttestationsAccumulator(included=0, assigned=0), # underperforming - ValidatorIndex(7): AttestationsAccumulator(included=900, assigned=1000), - ValidatorIndex(8): AttestationsAccumulator(included=500, assigned=1000), # underperforming - # ValidatorIndex(9): AttestationsAggregate(included=0, assigned=0), # missing in state - ValidatorIndex(10): AttestationsAccumulator(included=1000, assigned=1000), - ValidatorIndex(11): AttestationsAccumulator(included=1000, assigned=1000), - ValidatorIndex(12): AttestationsAccumulator(included=1000, assigned=1000), - }, - } - ) - module.w3.cc = Mock() - - module.converter = Mock( - side_effect=lambda _: Mock( - frame_config=FrameConfigFactory.build(epochs_per_frame=1000), - get_epoch_last_slot=lambda epoch: epoch * 32 + 31, - ) - ) - - module._get_ref_blockstamp_for_frame = Mock( - side_effect=[ - ReferenceBlockStampFactory.build( - slot_number=frame_0[1] * 32, ref_epoch=frame_0[1], ref_slot=frame_0[1] * 32 - ), - ReferenceBlockStampFactory.build(slot_number=r_epoch * 32, ref_epoch=r_epoch, ref_slot=r_epoch * 32), - ] - ) - - blockstamp = ReferenceBlockStampFactory.build(slot_number=r_epoch * 32, ref_epoch=r_epoch, ref_slot=r_epoch * 32) - distributed, shares, logs = module.calculate_distribution(blockstamp=blockstamp) - - assert distributed == 2 * 9_998 # because of the rounding - - assert tuple(shares.items()) == ( - (NodeOperatorId(0), 952), - (NodeOperatorId(1), 4761), - (NodeOperatorId(3), 4761), - (NodeOperatorId(6), 4761), - (NodeOperatorId(8), 4761), - ) - - assert len(logs) == 2 - - for log in logs: - - assert log.frame in module.state.data.keys() - assert log.threshold == module.state.get_network_aggr(log.frame).perf - 0.05 - - assert tuple(log.operators.keys()) == ( - NodeOperatorId(0), - NodeOperatorId(1), - NodeOperatorId(2), - NodeOperatorId(3), - NodeOperatorId(4), - NodeOperatorId(5), - NodeOperatorId(6), - # NodeOperatorId(7), # Missing in state - NodeOperatorId(8), - NodeOperatorId(9), - ) - - assert not log.operators[NodeOperatorId(1)].stuck - - assert log.operators[NodeOperatorId(2)].validators == {} - assert log.operators[NodeOperatorId(2)].stuck - assert log.operators[NodeOperatorId(4)].validators == {} - assert log.operators[NodeOperatorId(4)].stuck - - assert 5 in log.operators[NodeOperatorId(5)].validators - assert 6 in log.operators[NodeOperatorId(5)].validators - assert 7 in log.operators[NodeOperatorId(6)].validators - - assert log.operators[NodeOperatorId(0)].distributed == 476 - assert log.operators[NodeOperatorId(1)].distributed in [2380, 2381] - assert log.operators[NodeOperatorId(2)].distributed == 0 - assert log.operators[NodeOperatorId(3)].distributed in [2380, 2381] - assert log.operators[NodeOperatorId(6)].distributed in [2380, 2381] - - # Static functions you were dreaming of for so long. diff --git a/tests/modules/csm/test_state.py b/tests/modules/csm/test_state.py index b5d8f8808..ee09fe1e5 100644 --- a/tests/modules/csm/test_state.py +++ b/tests/modules/csm/test_state.py @@ -132,12 +132,16 @@ def test_clear_resets_state_to_empty(): def test_find_frame_returns_correct_frame(): state = State() + state._epochs_to_process = tuple(sequence(0, 31)) + state._epochs_per_frame = 32 state.data = {(0, 31): defaultdict(AttestationsAccumulator)} assert state.find_frame(15) == (0, 31) def test_find_frame_raises_error_for_out_of_range_epoch(): state = State() + state._epochs_to_process = tuple(sequence(0, 31)) + state._epochs_per_frame = 32 state.data = {(0, 31): defaultdict(AttestationsAccumulator)} with pytest.raises(ValueError, match="Epoch 32 is out of frames range"): state.find_frame(32) From f3896d4d2fb4b17277b6dc50eecb5450d957ac9c Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Thu, 13 Feb 2025 17:58:43 +0100 Subject: [PATCH 008/162] fix: log tests --- tests/modules/csm/test_log.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/modules/csm/test_log.py b/tests/modules/csm/test_log.py index 61004e9ed..c95ef93ca 100644 --- a/tests/modules/csm/test_log.py +++ b/tests/modules/csm/test_log.py @@ -28,7 +28,7 @@ def test_fields_access(log: FramePerfLog): def test_log_encode(log: FramePerfLog): # Fill in dynamic fields to make sure we have data in it to be encoded. - log.operators[NodeOperatorId(42)].validators["41337"].perf = AttestationsAccumulator(220, 119) + log.operators[NodeOperatorId(42)].validators["41337"].attestation_duty = AttestationsAccumulator(220, 119) log.operators[NodeOperatorId(42)].distributed = 17 log.operators[NodeOperatorId(0)].distributed = 0 @@ -37,8 +37,8 @@ def test_log_encode(log: FramePerfLog): encoded = FramePerfLog.encode(logs) for decoded in json.loads(encoded): - assert decoded["operators"]["42"]["validators"]["41337"]["perf"]["assigned"] == 220 - assert decoded["operators"]["42"]["validators"]["41337"]["perf"]["included"] == 119 + assert decoded["operators"]["42"]["validators"]["41337"]["attestation_duty"]["assigned"] == 220 + assert decoded["operators"]["42"]["validators"]["41337"]["attestation_duty"]["included"] == 119 assert decoded["operators"]["42"]["distributed"] == 17 assert decoded["operators"]["0"]["distributed"] == 0 @@ -51,12 +51,12 @@ def test_log_encode(log: FramePerfLog): def test_logs_encode(): log_0 = FramePerfLog(ReferenceBlockStampFactory.build(), (EpochNumber(100), EpochNumber(500))) - log_0.operators[NodeOperatorId(42)].validators["41337"].perf = AttestationsAccumulator(220, 119) + log_0.operators[NodeOperatorId(42)].validators["41337"].attestation_duty = AttestationsAccumulator(220, 119) log_0.operators[NodeOperatorId(42)].distributed = 17 log_0.operators[NodeOperatorId(0)].distributed = 0 log_1 = FramePerfLog(ReferenceBlockStampFactory.build(), (EpochNumber(500), EpochNumber(900))) - log_1.operators[NodeOperatorId(5)].validators["1234"].perf = AttestationsAccumulator(400, 399) + log_1.operators[NodeOperatorId(5)].validators["1234"].attestation_duty = AttestationsAccumulator(400, 399) log_1.operators[NodeOperatorId(5)].distributed = 40 log_1.operators[NodeOperatorId(18)].distributed = 0 @@ -68,13 +68,13 @@ def test_logs_encode(): assert len(decoded) == 2 - assert decoded[0]["operators"]["42"]["validators"]["41337"]["perf"]["assigned"] == 220 - assert decoded[0]["operators"]["42"]["validators"]["41337"]["perf"]["included"] == 119 + assert decoded[0]["operators"]["42"]["validators"]["41337"]["attestation_duty"]["assigned"] == 220 + assert decoded[0]["operators"]["42"]["validators"]["41337"]["attestation_duty"]["included"] == 119 assert decoded[0]["operators"]["42"]["distributed"] == 17 assert decoded[0]["operators"]["0"]["distributed"] == 0 - assert decoded[1]["operators"]["5"]["validators"]["1234"]["perf"]["assigned"] == 400 - assert decoded[1]["operators"]["5"]["validators"]["1234"]["perf"]["included"] == 399 + assert decoded[1]["operators"]["5"]["validators"]["1234"]["attestation_duty"]["assigned"] == 400 + assert decoded[1]["operators"]["5"]["validators"]["1234"]["attestation_duty"]["included"] == 399 assert decoded[1]["operators"]["5"]["distributed"] == 40 assert decoded[1]["operators"]["18"]["distributed"] == 0 From 27073f41b6d63a2c035e65deb2ef3aacc853a4a9 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Fri, 14 Feb 2025 09:22:29 +0100 Subject: [PATCH 009/162] fix: review --- src/modules/csm/state.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/modules/csm/state.py b/src/modules/csm/state.py index e6fb7d866..46f664157 100644 --- a/src/modules/csm/state.py +++ b/src/modules/csm/state.py @@ -117,12 +117,9 @@ def frames(self): @staticmethod def calculate_frames(epochs_to_process: tuple[EpochNumber, ...], epochs_per_frame: int) -> list[Frame]: """Split epochs to process into frames of `epochs_per_frame` length""" - frames = [] - for frame_epochs in batched(epochs_to_process, epochs_per_frame): - if len(frame_epochs) < epochs_per_frame: - raise ValueError("Insufficient epochs to form a frame") - frames.append((frame_epochs[0], frame_epochs[-1])) - return frames + if len(epochs_to_process) % epochs_per_frame != 0: + raise ValueError("Insufficient epochs to form a frame") + return [(frame[0], frame[-1]) for frame in batched(epochs_to_process, epochs_per_frame)] def clear(self) -> None: self.data = {} @@ -173,8 +170,7 @@ def init_or_migrate( for current_frame, migrated in migration_status.items(): if not migrated: logger.warning({"msg": f"Invalidating frame duties data cache: {current_frame}"}) - for epoch in sequence(*current_frame): - self._processed_epochs.discard(epoch) + self._processed_epochs.difference_update(sequence(*current_frame)) self.data = frames_data self._epochs_per_frame = epochs_per_frame @@ -201,9 +197,9 @@ def _migrate_frames_data( new_frame_l_epoch, new_frame_r_epoch = new_frame if curr_frame_l_epoch >= new_frame_l_epoch and curr_frame_r_epoch <= new_frame_r_epoch: logger.info({"msg": f"Migrating frame duties data cache: {current_frame=} -> {new_frame=}"}) - for val in self.data[current_frame]: - new_data[new_frame][val].assigned += self.data[current_frame][val].assigned - new_data[new_frame][val].included += self.data[current_frame][val].included + for val, duty in self.data[current_frame].items(): + new_data[new_frame][val].assigned += duty.assigned + new_data[new_frame][val].included += duty.included migration_status[current_frame] = True break From cbedd04871a7687e11a3062880f1330ca676653d Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Fri, 14 Feb 2025 13:08:24 +0100 Subject: [PATCH 010/162] fix: coverage + renaming --- src/modules/csm/csm.py | 8 ++++---- tests/modules/csm/test_csm_distribution.py | 8 ++++++-- tests/modules/csm/test_csm_module.py | 6 +++--- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index ed146677e..861e902e7 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -120,9 +120,9 @@ def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: ).as_tuple() if prev_cid and prev_root != ZERO_HASH: - # Update cumulative amount of shares for all operators. - for no_id, acc_shares in self.get_accumulated_shares(prev_cid, prev_root): - total_rewards[no_id] += acc_shares + # Update cumulative amount of stETH shares for all operators. + for no_id, accumulated_rewards in self.get_accumulated_rewards(prev_cid, prev_root): + total_rewards[no_id] += accumulated_rewards else: logger.info({"msg": "No previous distribution. Nothing to accumulate"}) @@ -355,7 +355,7 @@ def calc_rewards_distribution_in_frame( return rewards_distribution - def get_accumulated_shares(self, cid: CID, root: HexBytes) -> Iterator[tuple[NodeOperatorId, Shares]]: + def get_accumulated_rewards(self, cid: CID, root: HexBytes) -> Iterator[tuple[NodeOperatorId, Shares]]: logger.info({"msg": "Fetching tree by CID from IPFS", "cid": repr(cid)}) tree = Tree.decode(self.w3.ipfs.fetch(cid)) diff --git a/tests/modules/csm/test_csm_distribution.py b/tests/modules/csm/test_csm_distribution.py index cb07c2e95..f58545af3 100644 --- a/tests/modules/csm/test_csm_distribution.py +++ b/tests/modules/csm/test_csm_distribution.py @@ -1,5 +1,5 @@ from collections import defaultdict -from unittest.mock import Mock +from unittest.mock import Mock, call import pytest from web3.types import Wei @@ -10,6 +10,7 @@ from src.modules.csm.state import AttestationsAccumulator, State from src.types import NodeOperatorId, ValidatorIndex from src.web3py.extensions import CSM +from tests.factory.blockstamp import ReferenceBlockStampFactory from tests.factory.no_registry import LidoValidatorFactory @@ -42,7 +43,7 @@ def test_calculate_distribution_handles_single_frame(module): def test_calculate_distribution_handles_multiple_frames(module): module.state = Mock() module.state.frames = [(1, 2), (3, 4), (5, 6)] - blockstamp = Mock() + blockstamp = ReferenceBlockStampFactory.build(ref_epoch=2) module.module_validators_by_node_operators = Mock() module._get_ref_blockstamp_for_frame = Mock(return_value=blockstamp) module.w3.csm.fee_distributor.shares_to_distribute = Mock(return_value=800) @@ -59,6 +60,9 @@ def test_calculate_distribution_handles_multiple_frames(module): assert total_distributed == 800 assert total_rewards[NodeOperatorId(1)] == 800 assert len(logs) == 3 + module._get_ref_blockstamp_for_frame.assert_has_calls( + [call(blockstamp, frame[1]) for frame in module.state.frames[1:]] + ) def test_calculate_distribution_handles_invalid_distribution(module): diff --git a/tests/modules/csm/test_csm_module.py b/tests/modules/csm/test_csm_module.py index 1d396c083..b3f964a55 100644 --- a/tests/modules/csm/test_csm_module.py +++ b/tests/modules/csm/test_csm_module.py @@ -595,7 +595,7 @@ def test_build_report(csm: CSM, module: CSOracle, param: BuildReportTestParam): # mock previous report module.w3.csm.get_csm_tree_root = Mock(return_value=param.prev_tree_root) module.w3.csm.get_csm_tree_cid = Mock(return_value=param.prev_tree_cid) - module.get_accumulated_shares = Mock(return_value=param.prev_acc_shares) + module.get_accumulated_rewards = Mock(return_value=param.prev_acc_shares) # mock current frame module.calculate_distribution = param.curr_distribution module.make_tree = Mock(return_value=Mock(root=param.curr_tree_root)) @@ -654,7 +654,7 @@ def test_get_accumulated_shares(module: CSOracle, tree: Tree): encoded_tree = tree.encode() module.w3.ipfs = Mock(fetch=Mock(return_value=encoded_tree)) - for i, leaf in enumerate(module.get_accumulated_shares(cid=CIDv0("0x100500"), root=tree.root)): + for i, leaf in enumerate(module.get_accumulated_rewards(cid=CIDv0("0x100500"), root=tree.root)): assert tuple(leaf) == tree.tree.values[i]["value"] @@ -663,7 +663,7 @@ def test_get_accumulated_shares_unexpected_root(module: CSOracle, tree: Tree): module.w3.ipfs = Mock(fetch=Mock(return_value=encoded_tree)) with pytest.raises(ValueError): - next(module.get_accumulated_shares(cid=CIDv0("0x100500"), root=HexBytes("0x100500"))) + next(module.get_accumulated_rewards(cid=CIDv0("0x100500"), root=HexBytes("0x100500"))) @dataclass(frozen=True) From c83aa47f2a502c646d6b3045364d864b86b01543 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Fri, 14 Feb 2025 14:09:31 +0100 Subject: [PATCH 011/162] fix: get_stuck_operators --- src/modules/csm/csm.py | 32 ++++++++++----------- tests/modules/csm/test_csm_distribution.py | 8 +++--- tests/modules/csm/test_csm_module.py | 33 ++++++++++++---------- 3 files changed, 38 insertions(+), 35 deletions(-) diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index 861e902e7..4bdd5bd3c 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -160,7 +160,7 @@ def module_validators_by_node_operators(self, blockstamp: BlockStamp) -> Validat def validate_state(self, blockstamp: ReferenceBlockStamp) -> None: # NOTE: We cannot use `r_epoch` from the `current_frame_range` call because the `blockstamp` is a # `ReferenceBlockStamp`, hence it's a block the frame ends at. We use `ref_epoch` instead. - l_epoch, _ = self.current_frame_range(blockstamp) + l_epoch, _ = self.get_epochs_range_to_process(blockstamp) r_epoch = blockstamp.ref_epoch self.state.validate(l_epoch, r_epoch) @@ -175,8 +175,8 @@ def collect_data(self, blockstamp: BlockStamp) -> bool: converter = self.converter(blockstamp) - l_epoch, r_epoch = self.current_frame_range(blockstamp) - logger.info({"msg": f"Frame for performance data collect: epochs [{l_epoch};{r_epoch}]"}) + l_epoch, r_epoch = self.get_epochs_range_to_process(blockstamp) + logger.info({"msg": f"Epochs range for performance data collect: [{l_epoch};{r_epoch}]"}) # NOTE: Finalized slot is the first slot of justifying epoch, so we need to take the previous. But if the first # slot of the justifying epoch is empty, blockstamp.slot_number will point to the slot where the last finalized @@ -192,13 +192,13 @@ def collect_data(self, blockstamp: BlockStamp) -> bool: if report_blockstamp and report_blockstamp.ref_epoch != r_epoch: logger.warning( { - "msg": f"Frame has been changed, but the change is not yet observed on finalized epoch {finalized_epoch}" + "msg": f"Epochs range has been changed, but the change is not yet observed on finalized epoch {finalized_epoch}" } ) return False if l_epoch > finalized_epoch: - logger.info({"msg": "The starting epoch of the frame is not finalized yet"}) + logger.info({"msg": "The starting epoch of the epochs range is not finalized yet"}) return False self.state.init_or_migrate(l_epoch, r_epoch, converter.frame_config.epochs_per_frame, consensus_version) @@ -218,8 +218,8 @@ def collect_data(self, blockstamp: BlockStamp) -> bool: processor = FrameCheckpointProcessor(self.w3.cc, self.state, converter, blockstamp, eip7549_supported) for checkpoint in checkpoints: - if self.current_frame_range(self._receive_last_finalized_slot()) != (l_epoch, r_epoch): - logger.info({"msg": "Checkpoints were prepared for an outdated frame, stop processing"}) + if self.get_epochs_range_to_process(self._receive_last_finalized_slot()) != (l_epoch, r_epoch): + logger.info({"msg": "Checkpoints were prepared for an outdated epochs range, stop processing"}) raise ValueError("Outdated checkpoint") processor.exec(checkpoint) @@ -285,7 +285,7 @@ def _calculate_distribution_in_frame( participation_shares: defaultdict[NodeOperatorId, int] = defaultdict(int) - stuck_operators = self.stuck_operators(blockstamp) + stuck_operators = self.get_stuck_operators(frame, blockstamp) for (_, no_id), validators in operators_to_validators.items(): log_operator = log.operators[no_id] if no_id in stuck_operators: @@ -367,17 +367,17 @@ def get_accumulated_rewards(self, cid: CID, root: HexBytes) -> Iterator[tuple[No for v in tree.tree.values: yield v["value"] - def stuck_operators(self, blockstamp: ReferenceBlockStamp) -> set[NodeOperatorId]: + def get_stuck_operators(self, frame: Frame, frame_blockstamp: ReferenceBlockStamp) -> set[NodeOperatorId]: stuck: set[NodeOperatorId] = set() - l_epoch, _ = self.current_frame_range(blockstamp) - l_ref_slot = self.converter(blockstamp).get_epoch_first_slot(l_epoch) + l_epoch, _ = frame + l_ref_slot = self.converter(frame_blockstamp).get_epoch_first_slot(l_epoch) # NOTE: r_block is guaranteed to be <= ref_slot, and the check # in the inner frames assures the l_block <= r_block. l_blockstamp = build_blockstamp( get_next_non_missed_slot( self.w3.cc, l_ref_slot, - blockstamp.slot_number, + frame_blockstamp.slot_number, ) ) @@ -390,7 +390,7 @@ def stuck_operators(self, blockstamp: ReferenceBlockStamp) -> set[NodeOperatorId stuck.update( self.w3.csm.get_operators_with_stucks_in_range( l_blockstamp.block_hash, - blockstamp.block_hash, + frame_blockstamp.block_hash, ) ) return stuck @@ -424,7 +424,7 @@ def publish_log(self, logs: list[FramePerfLog]) -> CID: return log_cid @lru_cache(maxsize=1) - def current_frame_range(self, blockstamp: BlockStamp) -> tuple[EpochNumber, EpochNumber]: + def get_epochs_range_to_process(self, blockstamp: BlockStamp) -> tuple[EpochNumber, EpochNumber]: converter = self.converter(blockstamp) far_future_initial_epoch = converter.get_epoch_by_timestamp(UINT64_MAX) @@ -455,9 +455,9 @@ def current_frame_range(self, blockstamp: BlockStamp) -> tuple[EpochNumber, Epoc ) if l_ref_slot < last_processing_ref_slot: - raise CSMError(f"Got invalid frame range: {l_ref_slot=} < {last_processing_ref_slot=}") + raise CSMError(f"Got invalid epochs range: {l_ref_slot=} < {last_processing_ref_slot=}") if l_ref_slot >= r_ref_slot: - raise CSMError(f"Got invalid frame range {r_ref_slot=}, {l_ref_slot=}") + raise CSMError(f"Got invalid epochs range {r_ref_slot=}, {l_ref_slot=}") l_epoch = converter.get_epoch_by_slot(SlotNumber(l_ref_slot + 1)) r_epoch = converter.get_epoch_by_slot(r_ref_slot) diff --git a/tests/modules/csm/test_csm_distribution.py b/tests/modules/csm/test_csm_distribution.py index f58545af3..aac45d83e 100644 --- a/tests/modules/csm/test_csm_distribution.py +++ b/tests/modules/csm/test_csm_distribution.py @@ -85,7 +85,7 @@ def test_calculate_distribution_in_frame_handles_stuck_operator(module): operators_to_validators = {(Mock(), NodeOperatorId(1)): [LidoValidatorFactory.build()]} module.state = State() module.state.data = {frame: defaultdict(AttestationsAccumulator)} - module.stuck_operators = Mock(return_value={NodeOperatorId(1)}) + module.get_stuck_operators = Mock(return_value={NodeOperatorId(1)}) module._get_performance_threshold = Mock() rewards_distribution, log = module._calculate_distribution_in_frame( @@ -107,7 +107,7 @@ def test_calculate_distribution_in_frame_handles_no_attestation_duty(module): operators_to_validators = {(Mock(), node_operator_id): [validator]} module.state = State() module.state.data = {frame: defaultdict(AttestationsAccumulator)} - module.stuck_operators = Mock(return_value=set()) + module.get_stuck_operators = Mock(return_value=set()) module._get_performance_threshold = Mock() rewards_distribution, log = module._calculate_distribution_in_frame( @@ -131,7 +131,7 @@ def test_calculate_distribution_in_frame_handles_above_threshold_performance(mod module.state = State() attestation_duty = AttestationsAccumulator(assigned=10, included=6) module.state.data = {frame: {validator.index: attestation_duty}} - module.stuck_operators = Mock(return_value=set()) + module.get_stuck_operators = Mock(return_value=set()) module._get_performance_threshold = Mock(return_value=0.5) rewards_distribution, log = module._calculate_distribution_in_frame( @@ -155,7 +155,7 @@ def test_calculate_distribution_in_frame_handles_below_threshold_performance(mod module.state = State() attestation_duty = AttestationsAccumulator(assigned=10, included=5) module.state.data = {frame: {validator.index: attestation_duty}} - module.stuck_operators = Mock(return_value=set()) + module.get_stuck_operators = Mock(return_value=set()) module._get_performance_threshold = Mock(return_value=0.5) rewards_distribution, log = module._calculate_distribution_in_frame( diff --git a/tests/modules/csm/test_csm_module.py b/tests/modules/csm/test_csm_module.py index b3f964a55..1ef4c9234 100644 --- a/tests/modules/csm/test_csm_module.py +++ b/tests/modules/csm/test_csm_module.py @@ -39,7 +39,7 @@ def test_init(module: CSOracle): assert module -def test_stuck_operators(module: CSOracle, csm: CSM): +def test_get_stuck_operators(module: CSOracle, csm: CSM): module.module = Mock() # type: ignore module.module_id = StakingModuleId(1) module.w3.cc = Mock() @@ -66,7 +66,7 @@ def test_stuck_operators(module: CSOracle, csm: CSM): return_value=[NodeOperatorId(2), NodeOperatorId(4), NodeOperatorId(6), NodeOperatorId(1337)] ) - module.current_frame_range = Mock(return_value=(69, 100)) + module.get_epochs_range_to_process = Mock(return_value=(69, 100)) module.converter = Mock() module.converter.get_epoch_first_slot = Mock(return_value=lambda epoch: epoch * 32) @@ -78,12 +78,12 @@ def test_stuck_operators(module: CSOracle, csm: CSM): with patch('src.modules.csm.csm.build_blockstamp', return_value=l_blockstamp): with patch('src.modules.csm.csm.get_next_non_missed_slot', return_value=Mock()): - stuck = module.stuck_operators(blockstamp=blockstamp) + stuck = module.get_stuck_operators(frame=(69, 100), frame_blockstamp=blockstamp) assert stuck == {NodeOperatorId(2), NodeOperatorId(4), NodeOperatorId(5), NodeOperatorId(6), NodeOperatorId(1337)} -def test_stuck_operators_left_border_before_enact(module: CSOracle, csm: CSM, caplog: pytest.LogCaptureFixture): +def test_get_stuck_operators_left_border_before_enact(module: CSOracle, csm: CSM, caplog: pytest.LogCaptureFixture): module.module = Mock() # type: ignore module.module_id = StakingModuleId(3) module.w3.cc = Mock() @@ -112,7 +112,7 @@ def test_stuck_operators_left_border_before_enact(module: CSOracle, csm: CSM, ca ] ) - module.current_frame_range = Mock(return_value=(69, 100)) + module.get_epochs_range_to_process = Mock(return_value=(69, 100)) module.converter = Mock() module.converter.get_epoch_first_slot = Mock(return_value=lambda epoch: epoch * 32) @@ -121,7 +121,7 @@ def test_stuck_operators_left_border_before_enact(module: CSOracle, csm: CSM, ca with patch('src.modules.csm.csm.build_blockstamp', return_value=l_blockstamp): with patch('src.modules.csm.csm.get_next_non_missed_slot', return_value=Mock()): - stuck = module.stuck_operators(blockstamp=blockstamp) + stuck = module.get_stuck_operators(frame=(69, 100), frame_blockstamp=blockstamp) assert stuck == { NodeOperatorId(2), @@ -283,11 +283,11 @@ def test_current_frame_range(module: CSOracle, csm: CSM, mock_chain_config: NoRe if param.expected_frame is ValueError: with pytest.raises(ValueError): - module.current_frame_range(ReferenceBlockStampFactory.build(slot_number=param.finalized_slot)) + module.get_epochs_range_to_process(ReferenceBlockStampFactory.build(slot_number=param.finalized_slot)) else: bs = ReferenceBlockStampFactory.build(slot_number=param.finalized_slot) - l_epoch, r_epoch = module.current_frame_range(bs) + l_epoch, r_epoch = module.get_epochs_range_to_process(bs) assert (l_epoch, r_epoch) == param.expected_frame @@ -321,7 +321,7 @@ class CollectDataTestParam: collect_frame_range=Mock(return_value=(0, 1)), report_blockstamp=Mock(ref_epoch=3), state=Mock(), - expected_msg="Frame has been changed, but the change is not yet observed on finalized epoch 1", + expected_msg="Epochs range has been changed, but the change is not yet observed on finalized epoch 1", expected_result=False, ), id="frame_changed_forward", @@ -332,7 +332,7 @@ class CollectDataTestParam: collect_frame_range=Mock(return_value=(0, 2)), report_blockstamp=Mock(ref_epoch=1), state=Mock(), - expected_msg="Frame has been changed, but the change is not yet observed on finalized epoch 1", + expected_msg="Epochs range has been changed, but the change is not yet observed on finalized epoch 1", expected_result=False, ), id="frame_changed_backward", @@ -343,7 +343,7 @@ class CollectDataTestParam: collect_frame_range=Mock(return_value=(1, 2)), report_blockstamp=Mock(ref_epoch=2), state=Mock(), - expected_msg="The starting epoch of the frame is not finalized yet", + expected_msg="The starting epoch of the epochs range is not finalized yet", expected_result=False, ), id="starting_epoch_not_finalized", @@ -393,7 +393,7 @@ def test_collect_data( module.w3 = Mock() module._receive_last_finalized_slot = Mock() module.state = param.state - module.current_frame_range = param.collect_frame_range + module.get_epochs_range_to_process = param.collect_frame_range module.get_blockstamp_for_report = Mock(return_value=param.report_blockstamp) with caplog.at_level(logging.DEBUG): @@ -419,7 +419,7 @@ def test_collect_data_outdated_checkpoint( unprocessed_epochs=list(range(0, 101)), is_fulfilled=False, ) - module.current_frame_range = Mock(side_effect=[(0, 100), (50, 150)]) + module.get_epochs_range_to_process = Mock(side_effect=[(0, 100), (50, 150)]) module.get_blockstamp_for_report = Mock(return_value=Mock(ref_epoch=100)) with caplog.at_level(logging.DEBUG): @@ -427,7 +427,10 @@ def test_collect_data_outdated_checkpoint( module.collect_data(blockstamp=Mock(slot_number=640)) msg = list( - filter(lambda log: "Checkpoints were prepared for an outdated frame, stop processing" in log, caplog.messages) + filter( + lambda log: "Checkpoints were prepared for an outdated epochs range, stop processing" in log, + caplog.messages, + ) ) assert len(msg), "Expected message not found in logs" @@ -443,7 +446,7 @@ def test_collect_data_fulfilled_state( unprocessed_epochs=list(range(0, 101)), ) type(module.state).is_fulfilled = PropertyMock(side_effect=[False, True]) - module.current_frame_range = Mock(return_value=(0, 100)) + module.get_epochs_range_to_process = Mock(return_value=(0, 100)) module.get_blockstamp_for_report = Mock(return_value=Mock(ref_epoch=100)) with caplog.at_level(logging.DEBUG): From b58e7272a5dc81a822ed8bafe86b6e9a88477747 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Fri, 14 Feb 2025 18:03:28 +0100 Subject: [PATCH 012/162] refactor: get digests for `get_stuck_operators` --- src/modules/csm/csm.py | 30 ++++++++---------- tests/modules/csm/test_csm_module.py | 46 +++++++++------------------- 2 files changed, 27 insertions(+), 49 deletions(-) diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index 4bdd5bd3c..4781e6f00 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -28,7 +28,6 @@ ReferenceBlockStamp, SlotNumber, StakingModuleAddress, - StakingModuleId, ) from src.utils.blockstamp import build_blockstamp from src.utils.cache import global_lru_cache as lru_cache @@ -62,13 +61,13 @@ class CSOracle(BaseModule, ConsensusModule): COMPATIBLE_ONCHAIN_VERSIONS = [(1, 1), (1, 2)] report_contract: CSFeeOracleContract - module_id: StakingModuleId + staking_module: StakingModule def __init__(self, w3: Web3): self.report_contract = w3.csm.oracle self.state = State.load() super().__init__(w3) - self.module_id = self._get_module_id() + self.staking_module = self._get_staking_module() def refresh_contracts(self): self.report_contract = self.w3.csm.oracle # type: ignore @@ -368,7 +367,6 @@ def get_accumulated_rewards(self, cid: CID, root: HexBytes) -> Iterator[tuple[No yield v["value"] def get_stuck_operators(self, frame: Frame, frame_blockstamp: ReferenceBlockStamp) -> set[NodeOperatorId]: - stuck: set[NodeOperatorId] = set() l_epoch, _ = frame l_ref_slot = self.converter(frame_blockstamp).get_epoch_first_slot(l_epoch) # NOTE: r_block is guaranteed to be <= ref_slot, and the check @@ -381,19 +379,17 @@ def get_stuck_operators(self, frame: Frame, frame_blockstamp: ReferenceBlockStam ) ) - nos_by_module = self.w3.lido_validators.get_lido_node_operators_by_modules(l_blockstamp) - if self.module_id in nos_by_module: - stuck.update(no.id for no in nos_by_module[self.module_id] if no.stuck_validators_count > 0) - else: + digests = self.w3.lido_contracts.staking_router.get_all_node_operator_digests( + self.staking_module, l_blockstamp.block_hash + ) + if not digests: logger.warning("No CSM digest at blockstamp=%s, module was not added yet?", l_blockstamp) - - stuck.update( - self.w3.csm.get_operators_with_stucks_in_range( - l_blockstamp.block_hash, - frame_blockstamp.block_hash, - ) + stuck_from_digests = (no.id for no in digests if no.stuck_validators_count > 0) + stuck_from_events = self.w3.csm.get_operators_with_stucks_in_range( + l_blockstamp.block_hash, + frame_blockstamp.block_hash, ) - return stuck + return set(stuck_from_digests) | set(stuck_from_events) def make_tree(self, shares: dict[NodeOperatorId, Shares]) -> Tree: if not shares: @@ -471,11 +467,11 @@ def get_epochs_range_to_process(self, blockstamp: BlockStamp) -> tuple[EpochNumb def converter(self, blockstamp: BlockStamp) -> Web3Converter: return Web3Converter(self.get_chain_config(blockstamp), self.get_frame_config(blockstamp)) - def _get_module_id(self) -> StakingModuleId: + def _get_staking_module(self) -> StakingModule: modules: list[StakingModule] = self.w3.lido_contracts.staking_router.get_staking_modules() for mod in modules: if mod.staking_module_address == self.w3.csm.module.address: - return mod.id + return mod raise NoModuleFound diff --git a/tests/modules/csm/test_csm_module.py b/tests/modules/csm/test_csm_module.py index 1ef4c9234..8fd863cd5 100644 --- a/tests/modules/csm/test_csm_module.py +++ b/tests/modules/csm/test_csm_module.py @@ -9,12 +9,12 @@ from src.constants import UINT64_MAX from src.modules.csm.csm import CSOracle -from src.modules.csm.state import AttestationsAccumulator, State, Frame +from src.modules.csm.state import State from src.modules.csm.tree import Tree from src.modules.submodules.oracle_module import ModuleExecuteDelay from src.modules.submodules.types import CurrentFrame, ZERO_HASH from src.providers.ipfs import CIDv0, CID -from src.types import EpochNumber, NodeOperatorId, SlotNumber, StakingModuleId, ValidatorIndex +from src.types import NodeOperatorId, SlotNumber, StakingModuleId from src.web3py.extensions.csm import CSM from tests.factory.blockstamp import BlockStampFactory, ReferenceBlockStampFactory from tests.factory.configs import ChainConfigFactory, FrameConfigFactory @@ -22,7 +22,7 @@ @pytest.fixture(autouse=True) def mock_get_module_id(monkeypatch: pytest.MonkeyPatch): - monkeypatch.setattr(CSOracle, "_get_module_id", Mock()) + monkeypatch.setattr(CSOracle, "_get_staking_module", Mock()) @pytest.fixture(autouse=True) @@ -45,21 +45,16 @@ def test_get_stuck_operators(module: CSOracle, csm: CSM): module.w3.cc = Mock() module.w3.lido_validators = Mock() module.w3.lido_contracts = Mock() - module.w3.lido_validators.get_lido_node_operators_by_modules = Mock( - return_value={ - 1: { - type('NodeOperator', (object,), {'id': 0, 'stuck_validators_count': 0})(), - type('NodeOperator', (object,), {'id': 1, 'stuck_validators_count': 0})(), - type('NodeOperator', (object,), {'id': 2, 'stuck_validators_count': 1})(), - type('NodeOperator', (object,), {'id': 3, 'stuck_validators_count': 0})(), - type('NodeOperator', (object,), {'id': 4, 'stuck_validators_count': 100500})(), - type('NodeOperator', (object,), {'id': 5, 'stuck_validators_count': 100})(), - type('NodeOperator', (object,), {'id': 6, 'stuck_validators_count': 0})(), - }, - 2: {}, - 3: {}, - 4: {}, - } + module.w3.lido_contracts.staking_router.get_all_node_operator_digests = Mock( + return_value=[ + type('NodeOperator', (object,), {'id': 0, 'stuck_validators_count': 0})(), + type('NodeOperator', (object,), {'id': 1, 'stuck_validators_count': 0})(), + type('NodeOperator', (object,), {'id': 2, 'stuck_validators_count': 1})(), + type('NodeOperator', (object,), {'id': 3, 'stuck_validators_count': 0})(), + type('NodeOperator', (object,), {'id': 4, 'stuck_validators_count': 100500})(), + type('NodeOperator', (object,), {'id': 5, 'stuck_validators_count': 100})(), + type('NodeOperator', (object,), {'id': 6, 'stuck_validators_count': 0})(), + ] ) module.w3.csm.get_operators_with_stucks_in_range = Mock( @@ -89,20 +84,7 @@ def test_get_stuck_operators_left_border_before_enact(module: CSOracle, csm: CSM module.w3.cc = Mock() module.w3.lido_validators = Mock() module.w3.lido_contracts = Mock() - module.w3.lido_validators.get_lido_node_operators_by_modules = Mock( - return_value={ - 1: { - type('NodeOperator', (object,), {'id': 0, 'stuck_validators_count': 0})(), - type('NodeOperator', (object,), {'id': 1, 'stuck_validators_count': 0})(), - type('NodeOperator', (object,), {'id': 2, 'stuck_validators_count': 1})(), - type('NodeOperator', (object,), {'id': 3, 'stuck_validators_count': 0})(), - type('NodeOperator', (object,), {'id': 4, 'stuck_validators_count': 100500})(), - type('NodeOperator', (object,), {'id': 5, 'stuck_validators_count': 100})(), - type('NodeOperator', (object,), {'id': 6, 'stuck_validators_count': 0})(), - }, - 2: {}, - } - ) + module.w3.lido_contracts.staking_router.get_all_node_operator_digests = Mock(return_value=[]) module.w3.csm.get_operators_with_stucks_in_range = Mock( return_value=[ From bacfa9c76970981a9913596d95a944a0360deded Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Mon, 17 Feb 2025 11:02:29 +0100 Subject: [PATCH 013/162] fix: `mock_get_staking_module` --- tests/modules/csm/test_csm_distribution.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/modules/csm/test_csm_distribution.py b/tests/modules/csm/test_csm_distribution.py index aac45d83e..c2c04591a 100644 --- a/tests/modules/csm/test_csm_distribution.py +++ b/tests/modules/csm/test_csm_distribution.py @@ -6,7 +6,7 @@ from src.constants import UINT64_MAX from src.modules.csm.csm import CSOracle, CSMError -from src.modules.csm.log import ValidatorFrameSummary, OperatorFrameSummary +from src.modules.csm.log import ValidatorFrameSummary from src.modules.csm.state import AttestationsAccumulator, State from src.types import NodeOperatorId, ValidatorIndex from src.web3py.extensions import CSM @@ -15,8 +15,8 @@ @pytest.fixture(autouse=True) -def mock_get_module_id(monkeypatch: pytest.MonkeyPatch): - monkeypatch.setattr(CSOracle, "_get_module_id", Mock()) +def mock_get_staking_module(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(CSOracle, "_get_staking_module", Mock()) @pytest.fixture() From 21c8f1c7d13b1d209089e4b3c85f0542387bfe20 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Mon, 17 Feb 2025 16:11:00 +0100 Subject: [PATCH 014/162] chore: sync with base (with no new tests). WIP --- src/modules/csm/csm.py | 10 +- src/modules/csm/state.py | 69 ++-- tests/modules/csm/test_csm_distribution.py | 56 ++- tests/modules/csm/test_state.py | 432 ++++++++++++++++----- 4 files changed, 421 insertions(+), 146 deletions(-) diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index 3e31fd354..3a0ed5b87 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -14,8 +14,6 @@ from src.modules.csm.checkpoint import FrameCheckpointProcessor, FrameCheckpointsIterator, MinStepIsNotReached from src.modules.csm.log import FramePerfLog, OperatorFrameSummary from src.modules.csm.state import State, Frame, DutyAccumulator -from src.modules.csm.log import FramePerfLog, OperatorFrameSummary -from src.modules.csm.state import State, Frame from src.modules.csm.tree import Tree from src.modules.csm.types import ReportData, Shares from src.modules.submodules.consensus import ConsensusModule @@ -294,8 +292,12 @@ def _calculate_distribution_in_frame( log_operator.stuck = True continue for validator in validators: - duty = self.state.data[frame].get(validator.index) - self.process_validator_duty(validator, duty, threshold, participation_shares, log_operator) + att_duty = self.state.att_data[frame].get(validator.index) + prop_duty = self.state.prop_data[frame].get(validator.index) + sync_duty = self.state.sync_data[frame].get(validator.index) + self.process_validator_duty( + validator, att_duty, prop_duty, sync_duty, threshold, participation_shares, log_operator + ) rewards_distribution = self.calc_rewards_distribution_in_frame(participation_shares, rewards_to_distribute) diff --git a/src/modules/csm/state.py b/src/modules/csm/state.py index 84835fa8d..4d96496e2 100644 --- a/src/modules/csm/state.py +++ b/src/modules/csm/state.py @@ -178,8 +178,8 @@ def init_or_migrate( frames = self.calculate_frames(tuple(sequence(l_epoch, r_epoch)), epochs_per_frame) att_data = {frame: defaultdict(DutyAccumulator) for frame in frames} - sync_data = {frame: defaultdict(DutyAccumulator) for frame in frames} prop_data = {frame: defaultdict(DutyAccumulator) for frame in frames} + sync_data = {frame: defaultdict(DutyAccumulator) for frame in frames} if not self.is_empty: cached_frames = self.frames @@ -187,24 +187,30 @@ def init_or_migrate( logger.info({"msg": "No need to migrate duties data cache"}) return - frames_data, migration_status = self._migrate_frames_data(cached_frames, frames) + migration_status = self._migrate_frames_data(cached_frames, frames, att_data, prop_data, sync_data) for current_frame, migrated in migration_status.items(): if not migrated: logger.warning({"msg": f"Invalidating frame duties data cache: {current_frame}"}) self._processed_epochs.difference_update(sequence(*current_frame)) - self.data = frames_data + self.att_data = att_data + self.prop_data = prop_data + self.sync_data = sync_data self._epochs_per_frame = epochs_per_frame self._epochs_to_process = tuple(sequence(l_epoch, r_epoch)) self._consensus_version = consensus_version self.commit() def _migrate_frames_data( - self, current_frames: list[Frame], new_frames: list[Frame] - ) -> tuple[StateData, dict[Frame, bool]]: + self, + current_frames: list[Frame], + new_frames: list[Frame], + att_data: StateData, + prop_data: StateData, + sync_data: StateData + ) -> dict[Frame, bool]: migration_status = {frame: False for frame in current_frames} - new_data: StateData = {frame: defaultdict(DutyAccumulator) for frame in new_frames} logger.info({"msg": f"Migrating duties data cache: {current_frames=} -> {new_frames=}"}) @@ -212,20 +218,28 @@ def _migrate_frames_data( curr_frame_l_epoch, curr_frame_r_epoch = current_frame for new_frame in new_frames: if current_frame == new_frame: - new_data[new_frame] = self.data[current_frame] + att_data[new_frame] = self.att_data[current_frame] + prop_data[new_frame] = self.prop_data[current_frame] + sync_data[new_frame] = self.sync_data[current_frame] migration_status[current_frame] = True break new_frame_l_epoch, new_frame_r_epoch = new_frame if curr_frame_l_epoch >= new_frame_l_epoch and curr_frame_r_epoch <= new_frame_r_epoch: logger.info({"msg": f"Migrating frame duties data cache: {current_frame=} -> {new_frame=}"}) - for val, duty in self.data[current_frame].items(): - new_data[new_frame][val].assigned += duty.assigned - new_data[new_frame][val].included += duty.included + for val, duty in self.att_data[current_frame].items(): + att_data[new_frame][val].assigned += duty.assigned + att_data[new_frame][val].included += duty.included + for val, duty in self.prop_data[current_frame].items(): + prop_data[new_frame][val].assigned += duty.assigned + prop_data[new_frame][val].included += duty.included + for val, duty in self.sync_data[current_frame].items(): + sync_data[new_frame][val].assigned += duty.assigned + sync_data[new_frame][val].included += duty.included migration_status[current_frame] = True break - return new_data, migration_status + return migration_status def validate(self, l_epoch: EpochNumber, r_epoch: EpochNumber) -> None: if not self.is_fulfilled: @@ -241,45 +255,33 @@ def validate(self, l_epoch: EpochNumber, r_epoch: EpochNumber) -> None: def get_att_network_aggr(self, frame: Frame) -> DutyAccumulator: # TODO: exclude `active_slashed` validators from the calculation - included = assigned = 0 frame_data = self.att_data.get(frame) if frame_data is None: raise ValueError(f"No data for frame {frame} to calculate network aggregate") - for validator, acc in frame_data.items(): - if acc.included > acc.assigned: - raise ValueError(f"Invalid accumulator: {validator=}, {acc=}") - included += acc.included - assigned += acc.assigned - aggr = DutyAccumulator( - included=included, - assigned=assigned, - ) + aggr = self.get_duty_network_aggr(frame_data) logger.info({"msg": "Network attestations aggregate computed", "value": repr(aggr), "avg_perf": aggr.perf}) return aggr def get_sync_network_aggr(self, frame: Frame) -> DutyAccumulator: - included = assigned = 0 frame_data = self.sync_data.get(frame) if frame_data is None: raise ValueError(f"No data for frame {frame} to calculate syncs network aggregate") - for validator, acc in frame_data.items(): - if acc.included > acc.assigned: - raise ValueError(f"Invalid accumulator: {validator=}, {acc=}") - included += acc.included - assigned += acc.assigned - aggr = DutyAccumulator( - included=included, - assigned=assigned, - ) + aggr = self.get_duty_network_aggr(frame_data) logger.info({"msg": "Network syncs aggregate computed", "value": repr(aggr), "avg_perf": aggr.perf}) return aggr def get_prop_network_aggr(self, frame: Frame) -> DutyAccumulator: - included = assigned = 0 frame_data = self.prop_data.get(frame) if frame_data is None: raise ValueError(f"No data for frame {frame} to calculate proposal network aggregate") - for validator, acc in frame_data.items(): + aggr = self.get_duty_network_aggr(frame_data) + logger.info({"msg": "Network proposal aggregate computed", "value": repr(aggr), "avg_perf": aggr.perf}) + return aggr + + @staticmethod + def get_duty_network_aggr(duty_frame_data: defaultdict[ValidatorIndex, DutyAccumulator]) -> DutyAccumulator: + included = assigned = 0 + for validator, acc in duty_frame_data.items(): if acc.included > acc.assigned: raise ValueError(f"Invalid accumulator: {validator=}, {acc=}") included += acc.included @@ -288,5 +290,4 @@ def get_prop_network_aggr(self, frame: Frame) -> DutyAccumulator: included=included, assigned=assigned, ) - logger.info({"msg": "Network proposal aggregate computed", "value": repr(aggr), "avg_perf": aggr.perf}) return aggr diff --git a/tests/modules/csm/test_csm_distribution.py b/tests/modules/csm/test_csm_distribution.py index c2c04591a..2f264d70e 100644 --- a/tests/modules/csm/test_csm_distribution.py +++ b/tests/modules/csm/test_csm_distribution.py @@ -7,7 +7,7 @@ from src.constants import UINT64_MAX from src.modules.csm.csm import CSOracle, CSMError from src.modules.csm.log import ValidatorFrameSummary -from src.modules.csm.state import AttestationsAccumulator, State +from src.modules.csm.state import DutyAccumulator, State from src.types import NodeOperatorId, ValidatorIndex from src.web3py.extensions import CSM from tests.factory.blockstamp import ReferenceBlockStampFactory @@ -84,7 +84,7 @@ def test_calculate_distribution_in_frame_handles_stuck_operator(module): rewards_to_distribute = UINT64_MAX operators_to_validators = {(Mock(), NodeOperatorId(1)): [LidoValidatorFactory.build()]} module.state = State() - module.state.data = {frame: defaultdict(AttestationsAccumulator)} + module.state.data = {frame: defaultdict(DutyAccumulator)} module.get_stuck_operators = Mock(return_value={NodeOperatorId(1)}) module._get_performance_threshold = Mock() @@ -98,7 +98,7 @@ def test_calculate_distribution_in_frame_handles_stuck_operator(module): assert log.operators[NodeOperatorId(1)].validators == defaultdict(ValidatorFrameSummary) -def test_calculate_distribution_in_frame_handles_no_attestation_duty(module): +def test_calculate_distribution_in_frame_handles_no_any_duties(module): frame = Mock() blockstamp = Mock() rewards_to_distribute = UINT64_MAX @@ -106,7 +106,9 @@ def test_calculate_distribution_in_frame_handles_no_attestation_duty(module): node_operator_id = validator.lido_id.operatorIndex operators_to_validators = {(Mock(), node_operator_id): [validator]} module.state = State() - module.state.data = {frame: defaultdict(AttestationsAccumulator)} + module.state.att_data = {frame: defaultdict(DutyAccumulator)} + module.state.prop_data = {frame: defaultdict(DutyAccumulator)} + module.state.sync_data = {frame: defaultdict(DutyAccumulator)} module.get_stuck_operators = Mock(return_value=set()) module._get_performance_threshold = Mock() @@ -129,8 +131,12 @@ def test_calculate_distribution_in_frame_handles_above_threshold_performance(mod node_operator_id = validator.lido_id.operatorIndex operators_to_validators = {(Mock(), node_operator_id): [validator]} module.state = State() - attestation_duty = AttestationsAccumulator(assigned=10, included=6) - module.state.data = {frame: {validator.index: attestation_duty}} + attestation_duty = DutyAccumulator(assigned=10, included=6) + proposal_duty = DutyAccumulator(assigned=10, included=6) + sync_duty = DutyAccumulator(assigned=10, included=6) + module.state.att_data = {frame: {validator.index: attestation_duty}} + module.state.prop_data = {frame: {validator.index: proposal_duty}} + module.state.sync_data = {frame: {validator.index: sync_duty}} module.get_stuck_operators = Mock(return_value=set()) module._get_performance_threshold = Mock(return_value=0.5) @@ -142,6 +148,8 @@ def test_calculate_distribution_in_frame_handles_above_threshold_performance(mod assert log.operators[node_operator_id].stuck is False assert log.operators[node_operator_id].distributed > 0 assert log.operators[node_operator_id].validators[validator.index].attestation_duty == attestation_duty + assert log.operators[node_operator_id].validators[validator.index].proposal_duty == proposal_duty + assert log.operators[node_operator_id].validators[validator.index].sync_duty == sync_duty def test_calculate_distribution_in_frame_handles_below_threshold_performance(module): @@ -153,8 +161,12 @@ def test_calculate_distribution_in_frame_handles_below_threshold_performance(mod node_operator_id = validator.lido_id.operatorIndex operators_to_validators = {(Mock(), node_operator_id): [validator]} module.state = State() - attestation_duty = AttestationsAccumulator(assigned=10, included=5) - module.state.data = {frame: {validator.index: attestation_duty}} + attestation_duty = DutyAccumulator(assigned=10, included=5) + proposal_duty = DutyAccumulator(assigned=10, included=5) + sync_duty = DutyAccumulator(assigned=10, included=5) + module.state.att_data = {frame: {validator.index: attestation_duty}} + module.state.prop_data = {frame: {validator.index: proposal_duty}} + module.state.sync_data = {frame: {validator.index: sync_duty}} module.get_stuck_operators = Mock(return_value=set()) module._get_performance_threshold = Mock(return_value=0.5) @@ -166,14 +178,16 @@ def test_calculate_distribution_in_frame_handles_below_threshold_performance(mod assert log.operators[node_operator_id].stuck is False assert log.operators[node_operator_id].distributed == 0 assert log.operators[node_operator_id].validators[validator.index].attestation_duty == attestation_duty + assert log.operators[node_operator_id].validators[validator.index].proposal_duty == proposal_duty + assert log.operators[node_operator_id].validators[validator.index].sync_duty == sync_duty def test_performance_threshold_calculates_correctly(module): state = State() - state.data = { + state.att_data = { (0, 31): { - ValidatorIndex(1): AttestationsAccumulator(10, 10), - ValidatorIndex(2): AttestationsAccumulator(10, 10), + ValidatorIndex(1): DutyAccumulator(10, 10), + ValidatorIndex(2): DutyAccumulator(10, 10), }, } module.w3.csm.oracle.perf_leeway_bp.return_value = 500 @@ -188,8 +202,8 @@ def test_performance_threshold_handles_zero_leeway(module): state = State() state.data = { (0, 31): { - ValidatorIndex(1): AttestationsAccumulator(10, 10), - ValidatorIndex(2): AttestationsAccumulator(10, 10), + ValidatorIndex(1): DutyAccumulator(10, 10), + ValidatorIndex(2): DutyAccumulator(10, 10), }, } module.w3.csm.oracle.perf_leeway_bp.return_value = 0 @@ -203,7 +217,7 @@ def test_performance_threshold_handles_zero_leeway(module): def test_performance_threshold_handles_high_leeway(module): state = State() state.data = { - (0, 31): {ValidatorIndex(1): AttestationsAccumulator(10, 1), ValidatorIndex(2): AttestationsAccumulator(10, 1)}, + (0, 31): {ValidatorIndex(1): DutyAccumulator(10, 1), ValidatorIndex(2): DutyAccumulator(10, 1)}, } module.w3.csm.oracle.perf_leeway_bp.return_value = 5000 module.state = state @@ -221,7 +235,7 @@ def test_process_validator_duty_handles_above_threshold_performance(): participation_shares = defaultdict(int) threshold = 0.5 - attestation_duty = AttestationsAccumulator(assigned=10, included=6) + attestation_duty = DutyAccumulator(assigned=10, included=6) CSOracle.process_validator_duty(validator, attestation_duty, threshold, participation_shares, log_operator) @@ -237,7 +251,7 @@ def test_process_validator_duty_handles_below_threshold_performance(): participation_shares = defaultdict(int) threshold = 0.5 - attestation_duty = AttestationsAccumulator(assigned=10, included=4) + attestation_duty = DutyAccumulator(assigned=10, included=4) CSOracle.process_validator_duty(validator, attestation_duty, threshold, participation_shares, log_operator) @@ -253,7 +267,7 @@ def test_process_validator_duty_handles_non_empy_participation_shares(): participation_shares = {validator.lido_id.operatorIndex: 25} threshold = 0.5 - attestation_duty = AttestationsAccumulator(assigned=10, included=6) + attestation_duty = DutyAccumulator(assigned=10, included=6) CSOracle.process_validator_duty(validator, attestation_duty, threshold, participation_shares, log_operator) @@ -268,7 +282,7 @@ def test_process_validator_duty_handles_no_duty_assigned(): participation_shares = defaultdict(int) threshold = 0.5 - CSOracle.process_validator_duty(validator, None, threshold, participation_shares, log_operator) + CSOracle.process_validator_duty(validator, None, None, None, threshold, participation_shares, log_operator) assert participation_shares[validator.lido_id.operatorIndex] == 0 assert validator.index not in log_operator.validators @@ -282,9 +296,11 @@ def test_process_validator_duty_handles_slashed_validator(): participation_shares = defaultdict(int) threshold = 0.5 - attestation_duty = AttestationsAccumulator(assigned=1, included=1) + attestation_duty = DutyAccumulator(assigned=1, included=1) + prop_duty = DutyAccumulator(assigned=1, included=1) + sync_duty = DutyAccumulator(assigned=1, included=1) - CSOracle.process_validator_duty(validator, attestation_duty, threshold, participation_shares, log_operator) + CSOracle.process_validator_duty(validator, attestation_duty, prop_duty, sync_duty, threshold, participation_shares, log_operator) assert participation_shares[validator.lido_id.operatorIndex] == 0 assert log_operator.validators[validator.index].slashed is True diff --git a/tests/modules/csm/test_state.py b/tests/modules/csm/test_state.py index 121ffeb33..3b9f6e745 100644 --- a/tests/modules/csm/test_state.py +++ b/tests/modules/csm/test_state.py @@ -7,7 +7,7 @@ import pytest from src import variables -from src.modules.csm.state import State, InvalidState +from src.modules.csm.state import State, InvalidState, DutyAccumulator from src.types import ValidatorIndex from src.utils.range import sequence @@ -27,7 +27,7 @@ def test_load_restores_state_from_file(monkeypatch): monkeypatch.setattr("src.modules.csm.state.State.file", lambda _=None: Path("/tmp/state.pkl")) state = State() state.data = { - (0, 31): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}), + (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), } state.commit() loaded_state = State.load() @@ -51,7 +51,7 @@ def test_load_returns_new_instance_if_empty_object(monkeypatch, tmp_path): def test_commit_saves_state_to_file(monkeypatch): state = State() state.data = { - (0, 31): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}), + (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), } monkeypatch.setattr("src.modules.csm.state.State.file", lambda _: Path("/tmp/state.pkl")) monkeypatch.setattr("os.replace", Mock(side_effect=os.replace)) @@ -80,7 +80,9 @@ def test_is_empty_returns_true_for_empty_state(): def test_is_empty_returns_false_for_non_empty_state(): state = State() - state.data = {(0, 31): defaultdict(AttestationsAccumulator)} + state.att_data = {(0, 31): defaultdict(DutyAccumulator)} + state.prop_data = {(0, 31): defaultdict(DutyAccumulator)} + state.sync_data = {(0, 31): defaultdict(DutyAccumulator)} assert not state.is_empty @@ -125,7 +127,7 @@ def test_calculate_frames_raises_error_for_insufficient_epochs(): def test_clear_resets_state_to_empty(): state = State() - state.data = {(0, 31): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)})} + state.data = {(0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})} state.clear() assert state.is_empty @@ -134,7 +136,7 @@ def test_find_frame_returns_correct_frame(): state = State() state._epochs_to_process = tuple(sequence(0, 31)) state._epochs_per_frame = 32 - state.data = {(0, 31): defaultdict(AttestationsAccumulator)} + state.data = {(0, 31): defaultdict(DutyAccumulator)} assert state.find_frame(15) == (0, 31) @@ -142,51 +144,117 @@ def test_find_frame_raises_error_for_out_of_range_epoch(): state = State() state._epochs_to_process = tuple(sequence(0, 31)) state._epochs_per_frame = 32 - state.data = {(0, 31): defaultdict(AttestationsAccumulator)} + state.data = {(0, 31): defaultdict(DutyAccumulator)} with pytest.raises(ValueError, match="Epoch 32 is out of frames range"): state.find_frame(32) -def test_increment_duty_adds_duty_correctly(): +def test_increment_att_duty_adds_duty_correctly(): state = State() frame = (0, 31) - state.data = { - frame: defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}), + state.att_data = { + frame: defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), } - state.increment_duty(frame, ValidatorIndex(1), True) - assert state.data[frame][ValidatorIndex(1)].assigned == 11 - assert state.data[frame][ValidatorIndex(1)].included == 6 + state.increment_att_duty(frame, ValidatorIndex(1), True) + assert state.att_data[frame][ValidatorIndex(1)].assigned == 11 + assert state.att_data[frame][ValidatorIndex(1)].included == 6 -def test_increment_duty_creates_new_validator_entry(): +def test_increment_prop_duty_adds_duty_correctly(): state = State() frame = (0, 31) - state.data = { - frame: defaultdict(AttestationsAccumulator), + state.prop_data = { + frame: defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), } - state.increment_duty(frame, ValidatorIndex(2), True) - assert state.data[frame][ValidatorIndex(2)].assigned == 1 - assert state.data[frame][ValidatorIndex(2)].included == 1 + state.increment_prop_duty(frame, ValidatorIndex(1), True) + assert state.prop_data[frame][ValidatorIndex(1)].assigned == 11 + assert state.prop_data[frame][ValidatorIndex(1)].included == 6 -def test_increment_duty_handles_non_included_duty(): +def test_increment_sync_duty_adds_duty_correctly(): state = State() frame = (0, 31) - state.data = { - frame: defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}), + state.sync_data = { + frame: defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), + } + state.increment_sync_duty(frame, ValidatorIndex(1), True) + assert state.sync_data[frame][ValidatorIndex(1)].assigned == 11 + assert state.sync_data[frame][ValidatorIndex(1)].included == 6 + + +def test_increment_att_duty_creates_new_validator_entry(): + state = State() + frame = (0, 31) + state.att_data = { + frame: defaultdict(DutyAccumulator), } - state.increment_duty(frame, ValidatorIndex(1), False) - assert state.data[frame][ValidatorIndex(1)].assigned == 11 - assert state.data[frame][ValidatorIndex(1)].included == 5 + state.increment_att_duty(frame, ValidatorIndex(2), True) + assert state.att_data[frame][ValidatorIndex(2)].assigned == 1 + assert state.att_data[frame][ValidatorIndex(2)].included == 1 + + +def test_increment_prop_duty_creates_new_validator_entry(): + state = State() + frame = (0, 31) + state.prop_data = { + frame: defaultdict(DutyAccumulator), + } + state.increment_prop_duty(frame, ValidatorIndex(2), True) + assert state.prop_data[frame][ValidatorIndex(2)].assigned == 1 + assert state.prop_data[frame][ValidatorIndex(2)].included == 1 + + +def test_increment_sync_duty_creates_new_validator_entry(): + state = State() + frame = (0, 31) + state.sync_data = { + frame: defaultdict(DutyAccumulator), + } + state.increment_sync_duty(frame, ValidatorIndex(2), True) + assert state.sync_data[frame][ValidatorIndex(2)].assigned == 1 + assert state.sync_data[frame][ValidatorIndex(2)].included == 1 + + +def test_increment_att_duty_handles_non_included_duty(): + state = State() + frame = (0, 31) + state.att_data = { + frame: defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), + } + state.increment_att_duty(frame, ValidatorIndex(1), False) + assert state.att_data[frame][ValidatorIndex(1)].assigned == 11 + assert state.att_data[frame][ValidatorIndex(1)].included == 5 + + +def test_increment_prop_duty_handles_non_included_duty(): + state = State() + frame = (0, 31) + state.prop_data = { + frame: defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), + } + state.increment_prop_duty(frame, ValidatorIndex(1), False) + assert state.prop_data[frame][ValidatorIndex(1)].assigned == 11 + assert state.prop_data[frame][ValidatorIndex(1)].included == 5 + + +def test_increment_sync_duty_handles_non_included_duty(): + state = State() + frame = (0, 31) + state.sync_data = { + frame: defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), + } + state.increment_sync_duty(frame, ValidatorIndex(1), False) + assert state.sync_data[frame][ValidatorIndex(1)].assigned == 11 + assert state.sync_data[frame][ValidatorIndex(1)].included == 5 def test_increment_duty_raises_error_for_out_of_range_epoch(): state = State() - state.data = { - (0, 31): defaultdict(AttestationsAccumulator), + state.att_data = { + (0, 31): defaultdict(DutyAccumulator), } - with pytest.raises(ValueError, match="is not found in the state"): - state.increment_duty((0, 32), ValidatorIndex(1), True) + with pytest.raises(KeyError): + state.increment_att_duty((0, 32), ValidatorIndex(1), True) def test_add_processed_epoch_adds_epoch_to_processed_set(): @@ -218,8 +286,8 @@ def test_init_or_migrate_no_migration_needed(): state._epochs_to_process = tuple(sequence(0, 63)) state._epochs_per_frame = 32 state.data = { - (0, 31): defaultdict(AttestationsAccumulator), - (32, 63): defaultdict(AttestationsAccumulator), + (0, 31): defaultdict(DutyAccumulator), + (32, 63): defaultdict(DutyAccumulator), } state.commit = Mock() state.init_or_migrate(0, 63, 32, 1) @@ -231,14 +299,28 @@ def test_init_or_migrate_migrates_data(): state._consensus_version = 1 state._epochs_to_process = tuple(sequence(0, 63)) state._epochs_per_frame = 32 - state.data = { - (0, 31): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}), - (32, 63): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(20, 15)}), + state.att_data = { + (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), + (32, 63): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(20, 15)}), + } + state.prop_data = { + (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), + (32, 63): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(20, 15)}), + } + state.sync_data = { + (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), + (32, 63): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(20, 15)}), } state.commit = Mock() state.init_or_migrate(0, 63, 64, 1) - assert state.data == { - (0, 63): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(30, 20)}), + assert state.att_data == { + (0, 63): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(30, 20)}), + } + assert state.prop_data == { + (0, 63): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(30, 20)}), + } + assert state.sync_data == { + (0, 63): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(30, 20)}), } state.commit.assert_called_once() @@ -248,13 +330,25 @@ def test_init_or_migrate_invalidates_unmigrated_frames(): state._consensus_version = 1 state._epochs_to_process = tuple(sequence(0, 63)) state._epochs_per_frame = 64 - state.data = { - (0, 63): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(30, 20)}), + state.att_data = { + (0, 63): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(30, 20)}), + } + state.prop_data = { + (0, 63): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(30, 20)}), + } + state.sync_data = { + (0, 63): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(30, 20)}), } state.commit = Mock() state.init_or_migrate(0, 31, 32, 1) - assert state.data == { - (0, 31): defaultdict(AttestationsAccumulator), + assert state.att_data == { + (0, 31): defaultdict(DutyAccumulator), + } + assert state.prop_data == { + (0, 31): defaultdict(DutyAccumulator), + } + assert state.sync_data == { + (0, 31): defaultdict(DutyAccumulator), } assert state._processed_epochs == set() state.commit.assert_called_once() @@ -265,17 +359,35 @@ def test_init_or_migrate_discards_unmigrated_frame(): state._consensus_version = 1 state._epochs_to_process = tuple(sequence(0, 95)) state._epochs_per_frame = 32 - state.data = { - (0, 31): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}), - (32, 63): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(20, 15)}), - (64, 95): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(30, 25)}), + state.att_data = { + (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), + (32, 63): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(20, 15)}), + (64, 95): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(30, 25)}), + } + state.prop_data = { + (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), + (32, 63): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(20, 15)}), + (64, 95): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(30, 25)}), + } + state.sync_data = { + (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), + (32, 63): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(20, 15)}), + (64, 95): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(30, 25)}), } state._processed_epochs = set(sequence(0, 95)) state.commit = Mock() state.init_or_migrate(0, 63, 32, 1) - assert state.data == { - (0, 31): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}), - (32, 63): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(20, 15)}), + assert state.att_data == { + (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), + (32, 63): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(20, 15)}), + } + assert state.prop_data == { + (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), + (32, 63): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(20, 15)}), + } + assert state.sync_data == { + (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), + (32, 63): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(20, 15)}), } assert state._processed_epochs == set(sequence(0, 63)) state.commit.assert_called_once() @@ -285,13 +397,30 @@ def test_migrate_frames_data_creates_new_data_correctly(): state = State() current_frames = [(0, 31), (32, 63)] new_frames = [(0, 63)] - state.data = { - (0, 31): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}), - (32, 63): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(20, 15)}), + state.att_data = { + (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), + (32, 63): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(20, 15)}), } - new_data, migration_status = state._migrate_frames_data(current_frames, new_frames) - assert new_data == { - (0, 63): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(30, 20)}) + state.prop_data = { + (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), + (32, 63): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(20, 15)}), + } + state.sync_data = { + (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), + (32, 63): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(20, 15)}), + } + new_att = {(0, 63): defaultdict(DutyAccumulator)} + new_prop = {(0, 63): defaultdict(DutyAccumulator)} + new_sync = {(0, 63): defaultdict(DutyAccumulator)} + migration_status = state._migrate_frames_data(current_frames, new_frames, new_att, new_prop, new_sync) + assert new_att == { + (0, 63): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(30, 20)}) + } + assert new_prop == { + (0, 63): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(30, 20)}) + } + assert new_sync == { + (0, 63): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(30, 20)}) } assert migration_status == {(0, 31): True, (32, 63): True} @@ -300,12 +429,28 @@ def test_migrate_frames_data_handles_no_migration(): state = State() current_frames = [(0, 31)] new_frames = [(0, 31)] - state.data = { - (0, 31): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}), + state.att_data = { + (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), + } + state.prop_data = { + (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), } - new_data, migration_status = state._migrate_frames_data(current_frames, new_frames) - assert new_data == { - (0, 31): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}) + state.sync_data = { + (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), + } + new_att = {(0, 31): defaultdict(DutyAccumulator)} + new_prop = {(0, 31): defaultdict(DutyAccumulator)} + new_sync = {(0, 31): defaultdict(DutyAccumulator)} + + migration_status = state._migrate_frames_data(current_frames, new_frames, new_att, new_prop, new_sync) + assert new_att == { + (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}) + } + assert new_prop == { + (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}) + } + assert new_sync == { + (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}) } assert migration_status == {(0, 31): True} @@ -314,14 +459,33 @@ def test_migrate_frames_data_handles_partial_migration(): state = State() current_frames = [(0, 31), (32, 63)] new_frames = [(0, 31), (32, 95)] - state.data = { - (0, 31): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}), - (32, 63): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(20, 15)}), + state.att_data = { + (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), + (32, 63): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(20, 15)}), + } + state.prop_data = { + (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), + (32, 63): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(20, 15)}), + } + state.sync_data = { + (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), + (32, 63): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(20, 15)}), + } + new_att = {(0, 31): defaultdict(DutyAccumulator), (32, 95): defaultdict(DutyAccumulator)} + new_prop = {(0, 31): defaultdict(DutyAccumulator), (32, 95): defaultdict(DutyAccumulator)} + new_sync = {(0, 31): defaultdict(DutyAccumulator), (32, 95): defaultdict(DutyAccumulator)} + migration_status = state._migrate_frames_data(current_frames, new_frames, new_att, new_prop, new_sync) + assert new_att == { + (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), + (32, 95): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(20, 15)}), } - new_data, migration_status = state._migrate_frames_data(current_frames, new_frames) - assert new_data == { - (0, 31): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}), - (32, 95): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(20, 15)}), + assert new_prop == { + (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), + (32, 95): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(20, 15)}), + } + assert new_sync == { + (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), + (32, 95): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(20, 15)}), } assert migration_status == {(0, 31): True, (32, 63): True} @@ -330,9 +494,16 @@ def test_migrate_frames_data_handles_no_data(): state = State() current_frames = [(0, 31)] new_frames = [(0, 31)] - state.data = {frame: defaultdict(AttestationsAccumulator) for frame in current_frames} - new_data, migration_status = state._migrate_frames_data(current_frames, new_frames) - assert new_data == {(0, 31): defaultdict(AttestationsAccumulator)} + state.att_data = {frame: defaultdict(DutyAccumulator) for frame in current_frames} + state.prop_data = {frame: defaultdict(DutyAccumulator) for frame in current_frames} + state.sync_data = {frame: defaultdict(DutyAccumulator) for frame in current_frames} + new_att = {frame: defaultdict(DutyAccumulator) for frame in new_frames} + new_prop = {frame: defaultdict(DutyAccumulator) for frame in new_frames} + new_sync = {frame: defaultdict(DutyAccumulator) for frame in new_frames} + migration_status = state._migrate_frames_data(current_frames, new_frames, new_att, new_prop, new_sync) + assert new_att == {(0, 31): defaultdict(DutyAccumulator)} + assert new_prop == {(0, 31): defaultdict(DutyAccumulator)} + assert new_sync == {(0, 31): defaultdict(DutyAccumulator)} assert migration_status == {(0, 31): True} @@ -340,13 +511,30 @@ def test_migrate_frames_data_handles_wider_old_frame(): state = State() current_frames = [(0, 63)] new_frames = [(0, 31), (32, 63)] - state.data = { - (0, 63): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(30, 20)}), + state.att_data = { + (0, 63): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(30, 20)}), + } + state.prop_data = { + (0, 63): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(30, 20)}), + } + state.sync_data = { + (0, 63): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(30, 20)}), } - new_data, migration_status = state._migrate_frames_data(current_frames, new_frames) - assert new_data == { - (0, 31): defaultdict(AttestationsAccumulator), - (32, 63): defaultdict(AttestationsAccumulator), + new_att = {frame: defaultdict(DutyAccumulator) for frame in new_frames} + new_prop = {frame: defaultdict(DutyAccumulator) for frame in new_frames} + new_sync = {frame: defaultdict(DutyAccumulator) for frame in new_frames} + migration_status = state._migrate_frames_data(current_frames, new_frames, new_att, new_prop, new_sync) + assert new_att == { + (0, 31): defaultdict(DutyAccumulator), + (32, 63): defaultdict(DutyAccumulator), + } + assert new_prop == { + (0, 31): defaultdict(DutyAccumulator), + (32, 63): defaultdict(DutyAccumulator), + } + assert new_sync == { + (0, 31): defaultdict(DutyAccumulator), + (32, 63): defaultdict(DutyAccumulator), } assert migration_status == {(0, 63): False} @@ -384,39 +572,107 @@ def test_validate_passes_for_fulfilled_state(): def test_attestation_aggregate_perf(): - aggr = AttestationsAccumulator(included=333, assigned=777) + aggr = DutyAccumulator(included=333, assigned=777) assert aggr.perf == pytest.approx(0.4285, abs=1e-4) -def test_get_network_aggr_computes_correctly(): +def test_get_att_network_aggr_computes_correctly(): state = State() - state.data = { + state.att_data = { + (0, 31): defaultdict( + DutyAccumulator, + {ValidatorIndex(1): DutyAccumulator(10, 5), ValidatorIndex(2): DutyAccumulator(20, 15)}, + ) + } + aggr = state.get_att_network_aggr((0, 31)) + assert aggr.assigned == 30 + assert aggr.included == 20 + + +def test_get_sync_network_aggr_computes_correctly(): + state = State() + state.sync_data = { + (0, 31): defaultdict( + DutyAccumulator, + {ValidatorIndex(1): DutyAccumulator(10, 5), ValidatorIndex(2): DutyAccumulator(20, 15)}, + ) + } + aggr = state.get_sync_network_aggr((0, 31)) + assert aggr.assigned == 30 + assert aggr.included == 20 + + +def test_get_prop_network_aggr_computes_correctly(): + state = State() + state.prop_data = { (0, 31): defaultdict( - AttestationsAccumulator, - {ValidatorIndex(1): AttestationsAccumulator(10, 5), ValidatorIndex(2): AttestationsAccumulator(20, 15)}, + DutyAccumulator, + {ValidatorIndex(1): DutyAccumulator(10, 5), ValidatorIndex(2): DutyAccumulator(20, 15)}, ) } - aggr = state.get_network_aggr((0, 31)) + aggr = state.get_prop_network_aggr((0, 31)) assert aggr.assigned == 30 assert aggr.included == 20 -def test_get_network_aggr_raises_error_for_invalid_accumulator(): +def test_get_att_network_aggr_raises_error_for_invalid_accumulator(): + state = State() + state.att_data = {(0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 15)})} + with pytest.raises(ValueError, match="Invalid accumulator"): + state.get_att_network_aggr((0, 31)) + + +def test_get_prop_network_aggr_raises_error_for_invalid_accumulator(): + state = State() + state.prop_data = {(0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 15)})} + with pytest.raises(ValueError, match="Invalid accumulator"): + state.get_prop_network_aggr((0, 31)) + + +def test_get_sync_network_aggr_raises_error_for_invalid_accumulator(): state = State() - state.data = {(0, 31): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 15)})} + state.sync_data = {(0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 15)})} with pytest.raises(ValueError, match="Invalid accumulator"): - state.get_network_aggr((0, 31)) + state.get_sync_network_aggr((0, 31)) + + +def test_get_att_network_aggr_raises_error_for_missing_frame_data(): + state = State() + with pytest.raises(ValueError, match="No data for frame"): + state.get_att_network_aggr((0, 31)) -def test_get_network_aggr_raises_error_for_missing_frame_data(): +def test_get_prop_network_aggr_raises_error_for_missing_frame_data(): state = State() with pytest.raises(ValueError, match="No data for frame"): - state.get_network_aggr((0, 31)) + state.get_prop_network_aggr((0, 31)) + + +def test_get_sync_network_aggr_raises_error_for_missing_frame_data(): + state = State() + with pytest.raises(ValueError, match="No data for frame"): + state.get_sync_network_aggr((0, 31)) + + +def test_get_att_network_aggr_handles_empty_frame_data(): + state = State() + state.att_data = {(0, 31): defaultdict(DutyAccumulator)} + aggr = state.get_att_network_aggr((0, 31)) + assert aggr.assigned == 0 + assert aggr.included == 0 + + +def test_get_prop_network_aggr_handles_empty_frame_data(): + state = State() + state.prop_data = {(0, 31): defaultdict(DutyAccumulator)} + aggr = state.get_prop_network_aggr((0, 31)) + assert aggr.assigned == 0 + assert aggr.included == 0 -def test_get_network_aggr_handles_empty_frame_data(): +def test_get_sync_network_aggr_handles_empty_frame_data(): state = State() - state.data = {(0, 31): defaultdict(AttestationsAccumulator)} - aggr = state.get_network_aggr((0, 31)) + state.sync_data = {(0, 31): defaultdict(DutyAccumulator)} + aggr = state.get_sync_network_aggr((0, 31)) assert aggr.assigned == 0 assert aggr.included == 0 From 00bba7c4e826fa93a5b7aa8d0f9c26fde8028218 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Tue, 18 Feb 2025 18:00:49 +0100 Subject: [PATCH 015/162] wip: smoke --- assets/CSFeeOracle.json | 2 +- assets/CSModule.json | 2 +- assets/CSParametersRegistry.json | 1 + src/constants.py | 1 + src/modules/csm/csm.py | 241 ++------------- src/modules/csm/distribution.py | 286 ++++++++++++++++++ src/modules/csm/log.py | 16 +- src/modules/csm/types.py | 6 + .../execution/contracts/cs_accounting.py | 14 + .../execution/contracts/cs_module.py | 13 + .../contracts/cs_parameters_registry.py | 82 +++++ src/web3py/extensions/csm.py | 16 +- 12 files changed, 456 insertions(+), 224 deletions(-) create mode 100644 assets/CSParametersRegistry.json create mode 100644 src/modules/csm/distribution.py create mode 100644 src/providers/execution/contracts/cs_parameters_registry.py diff --git a/assets/CSFeeOracle.json b/assets/CSFeeOracle.json index 4c0e68d3d..c9bd74471 100644 --- a/assets/CSFeeOracle.json +++ b/assets/CSFeeOracle.json @@ -1 +1 @@ -[{"type":"constructor","inputs":[{"name":"secondsPerSlot","type":"uint256","internalType":"uint256"},{"name":"genesisTime","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"CONTRACT_MANAGER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"GENESIS_TIME","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"MANAGE_CONSENSUS_CONTRACT_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"MANAGE_CONSENSUS_VERSION_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_INFINITELY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RECOVERER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RESUME_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"SECONDS_PER_SLOT","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"SUBMIT_DATA_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"avgPerfLeewayBP","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"discardConsensusReport","inputs":[{"name":"refSlot","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"feeDistributor","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSFeeDistributor"}],"stateMutability":"view"},{"type":"function","name":"getConsensusContract","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getConsensusReport","inputs":[],"outputs":[{"name":"hash","type":"bytes32","internalType":"bytes32"},{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"processingDeadlineTime","type":"uint256","internalType":"uint256"},{"name":"processingStarted","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"getConsensusVersion","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getContractVersion","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getLastProcessingRefSlot","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getResumeSinceTimestamp","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"admin","type":"address","internalType":"address"},{"name":"feeDistributorContract","type":"address","internalType":"address"},{"name":"consensusContract","type":"address","internalType":"address"},{"name":"consensusVersion","type":"uint256","internalType":"uint256"},{"name":"_avgPerfLeewayBP","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"isPaused","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"pauseFor","inputs":[{"name":"duration","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"pauseUntil","inputs":[{"name":"pauseUntilInclusive","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC1155","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC20","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC721","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverEther","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resume","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setConsensusContract","inputs":[{"name":"addr","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setConsensusVersion","inputs":[{"name":"version","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setFeeDistributorContract","inputs":[{"name":"feeDistributorContract","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setPerformanceLeeway","inputs":[{"name":"valueBP","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"submitConsensusReport","inputs":[{"name":"reportHash","type":"bytes32","internalType":"bytes32"},{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"submitReportData","inputs":[{"name":"data","type":"tuple","internalType":"struct CSFeeOracle.ReportData","components":[{"name":"consensusVersion","type":"uint256","internalType":"uint256"},{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"treeRoot","type":"bytes32","internalType":"bytes32"},{"name":"treeCid","type":"string","internalType":"string"},{"name":"logCid","type":"string","internalType":"string"},{"name":"distributed","type":"uint256","internalType":"uint256"}]},{"name":"contractVersion","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"event","name":"ConsensusHashContractSet","inputs":[{"name":"addr","type":"address","indexed":true,"internalType":"address"},{"name":"prevAddr","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"ConsensusVersionSet","inputs":[{"name":"version","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"prevVersion","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ContractVersionSet","inputs":[{"name":"version","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC1155Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC20Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC721Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"EtherRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"FeeDistributorContractSet","inputs":[{"name":"feeDistributorContract","type":"address","indexed":false,"internalType":"address"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"Paused","inputs":[{"name":"duration","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PerfLeewaySet","inputs":[{"name":"valueBP","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ProcessingStarted","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"hash","type":"bytes32","indexed":false,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"ReportDiscarded","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"hash","type":"bytes32","indexed":false,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"ReportSettled","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ReportSubmitted","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"hash","type":"bytes32","indexed":false,"internalType":"bytes32"},{"name":"processingDeadlineTime","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Resumed","inputs":[],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"StETHSharesRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"WarnProcessingMissed","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"AddressCannotBeSame","inputs":[]},{"type":"error","name":"AddressCannotBeZero","inputs":[]},{"type":"error","name":"FailedToSendEther","inputs":[]},{"type":"error","name":"HashCannotBeZero","inputs":[]},{"type":"error","name":"InitialRefSlotCannotBeLessThanProcessingOne","inputs":[{"name":"initialRefSlot","type":"uint256","internalType":"uint256"},{"name":"processingRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"InvalidContractVersion","inputs":[]},{"type":"error","name":"InvalidContractVersionIncrement","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidPerfLeeway","inputs":[]},{"type":"error","name":"NoConsensusReportToProcess","inputs":[]},{"type":"error","name":"NonZeroContractVersionOnInit","inputs":[]},{"type":"error","name":"NotAllowedToRecover","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"PauseUntilMustBeInFuture","inputs":[]},{"type":"error","name":"PausedExpected","inputs":[]},{"type":"error","name":"ProcessingDeadlineMissed","inputs":[{"name":"deadline","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"RefSlotAlreadyProcessing","inputs":[]},{"type":"error","name":"RefSlotCannotDecrease","inputs":[{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"prevRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"RefSlotMustBeGreaterThanProcessingOne","inputs":[{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"processingRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"ResumedExpected","inputs":[]},{"type":"error","name":"SafeCastOverflowedUintDowncast","inputs":[{"name":"bits","type":"uint8","internalType":"uint8"},{"name":"value","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"SecondsPerSlotCannotBeZero","inputs":[]},{"type":"error","name":"SenderIsNotTheConsensusContract","inputs":[]},{"type":"error","name":"SenderNotAllowed","inputs":[]},{"type":"error","name":"UnexpectedChainConfig","inputs":[]},{"type":"error","name":"UnexpectedConsensusVersion","inputs":[{"name":"expectedVersion","type":"uint256","internalType":"uint256"},{"name":"receivedVersion","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"UnexpectedContractVersion","inputs":[{"name":"expected","type":"uint256","internalType":"uint256"},{"name":"received","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"UnexpectedDataHash","inputs":[{"name":"consensusHash","type":"bytes32","internalType":"bytes32"},{"name":"receivedHash","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"UnexpectedRefSlot","inputs":[{"name":"consensusRefSlot","type":"uint256","internalType":"uint256"},{"name":"dataRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"VersionCannotBeSame","inputs":[]},{"type":"error","name":"VersionCannotBeZero","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroFeeDistributorAddress","inputs":[]},{"type":"error","name":"ZeroPauseDuration","inputs":[]}] +[{"type":"constructor","inputs":[{"name":"secondsPerSlot","type":"uint256","internalType":"uint256"},{"name":"genesisTime","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"GENESIS_TIME","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"MANAGE_CONSENSUS_CONTRACT_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"MANAGE_CONSENSUS_VERSION_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_INFINITELY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RECOVERER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RESUME_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"SECONDS_PER_SLOT","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"SUBMIT_DATA_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"discardConsensusReport","inputs":[{"name":"refSlot","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"feeDistributor","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSFeeDistributor"}],"stateMutability":"view"},{"type":"function","name":"finalizeUpgradeV2","inputs":[{"name":"consensusVersion","type":"uint256","internalType":"uint256"},{"name":"strikesContract","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"getConsensusContract","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getConsensusReport","inputs":[],"outputs":[{"name":"hash","type":"bytes32","internalType":"bytes32"},{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"processingDeadlineTime","type":"uint256","internalType":"uint256"},{"name":"processingStarted","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"getConsensusVersion","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getContractVersion","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getLastProcessingRefSlot","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getResumeSinceTimestamp","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"admin","type":"address","internalType":"address"},{"name":"feeDistributorContract","type":"address","internalType":"address"},{"name":"strikesContract","type":"address","internalType":"address"},{"name":"consensusContract","type":"address","internalType":"address"},{"name":"consensusVersion","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"isPaused","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"pauseFor","inputs":[{"name":"duration","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"pauseUntil","inputs":[{"name":"pauseUntilInclusive","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC1155","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC20","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC721","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverEther","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resume","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setConsensusContract","inputs":[{"name":"addr","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setConsensusVersion","inputs":[{"name":"version","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setFeeDistributorContract","inputs":[{"name":"feeDistributorContract","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setStrikesContract","inputs":[{"name":"strikesContract","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"strikes","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSStrikes"}],"stateMutability":"view"},{"type":"function","name":"submitConsensusReport","inputs":[{"name":"reportHash","type":"bytes32","internalType":"bytes32"},{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"submitReportData","inputs":[{"name":"data","type":"tuple","internalType":"struct ICSFeeOracle.ReportData","components":[{"name":"consensusVersion","type":"uint256","internalType":"uint256"},{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"treeRoot","type":"bytes32","internalType":"bytes32"},{"name":"treeCid","type":"string","internalType":"string"},{"name":"logCid","type":"string","internalType":"string"},{"name":"distributed","type":"uint256","internalType":"uint256"},{"name":"rebate","type":"uint256","internalType":"uint256"},{"name":"strikesTreeRoot","type":"bytes32","internalType":"bytes32"},{"name":"strikesTreeCid","type":"string","internalType":"string"}]},{"name":"contractVersion","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"event","name":"ConsensusHashContractSet","inputs":[{"name":"addr","type":"address","indexed":true,"internalType":"address"},{"name":"prevAddr","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"ConsensusVersionSet","inputs":[{"name":"version","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"prevVersion","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ContractVersionSet","inputs":[{"name":"version","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC1155Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC20Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC721Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"EtherRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"FeeDistributorContractSet","inputs":[{"name":"feeDistributorContract","type":"address","indexed":false,"internalType":"address"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"Paused","inputs":[{"name":"duration","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ProcessingStarted","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"hash","type":"bytes32","indexed":false,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"ReportDiscarded","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"hash","type":"bytes32","indexed":false,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"ReportSubmitted","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"hash","type":"bytes32","indexed":false,"internalType":"bytes32"},{"name":"processingDeadlineTime","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Resumed","inputs":[],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"StETHSharesRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"StrikesContractSet","inputs":[{"name":"strikesContract","type":"address","indexed":false,"internalType":"address"}],"anonymous":false},{"type":"event","name":"WarnProcessingMissed","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"AddressCannotBeSame","inputs":[]},{"type":"error","name":"AddressCannotBeZero","inputs":[]},{"type":"error","name":"FailedToSendEther","inputs":[]},{"type":"error","name":"HashCannotBeZero","inputs":[]},{"type":"error","name":"InitialRefSlotCannotBeLessThanProcessingOne","inputs":[{"name":"initialRefSlot","type":"uint256","internalType":"uint256"},{"name":"processingRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"InvalidContractVersion","inputs":[]},{"type":"error","name":"InvalidContractVersionIncrement","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"NoConsensusReportToProcess","inputs":[]},{"type":"error","name":"NonZeroContractVersionOnInit","inputs":[]},{"type":"error","name":"NotAllowedToRecover","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"PauseUntilMustBeInFuture","inputs":[]},{"type":"error","name":"PausedExpected","inputs":[]},{"type":"error","name":"ProcessingDeadlineMissed","inputs":[{"name":"deadline","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"RefSlotAlreadyProcessing","inputs":[]},{"type":"error","name":"RefSlotCannotDecrease","inputs":[{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"prevRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"RefSlotMustBeGreaterThanProcessingOne","inputs":[{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"processingRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"ResumedExpected","inputs":[]},{"type":"error","name":"SafeCastOverflowedUintDowncast","inputs":[{"name":"bits","type":"uint8","internalType":"uint8"},{"name":"value","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"SecondsPerSlotCannotBeZero","inputs":[]},{"type":"error","name":"SenderIsNotTheConsensusContract","inputs":[]},{"type":"error","name":"SenderNotAllowed","inputs":[]},{"type":"error","name":"UnexpectedChainConfig","inputs":[]},{"type":"error","name":"UnexpectedConsensusVersion","inputs":[{"name":"expectedVersion","type":"uint256","internalType":"uint256"},{"name":"receivedVersion","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"UnexpectedContractVersion","inputs":[{"name":"expected","type":"uint256","internalType":"uint256"},{"name":"received","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"UnexpectedDataHash","inputs":[{"name":"consensusHash","type":"bytes32","internalType":"bytes32"},{"name":"receivedHash","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"UnexpectedRefSlot","inputs":[{"name":"consensusRefSlot","type":"uint256","internalType":"uint256"},{"name":"dataRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"VersionCannotBeSame","inputs":[]},{"type":"error","name":"VersionCannotBeZero","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroFeeDistributorAddress","inputs":[]},{"type":"error","name":"ZeroPauseDuration","inputs":[]},{"type":"error","name":"ZeroStrikesAddress","inputs":[]}] \ No newline at end of file diff --git a/assets/CSModule.json b/assets/CSModule.json index 02b75373e..53518035f 100644 --- a/assets/CSModule.json +++ b/assets/CSModule.json @@ -1 +1 @@ -[{"type":"constructor","inputs":[{"name":"moduleType","type":"bytes32","internalType":"bytes32"},{"name":"minSlashingPenaltyQuotient","type":"uint256","internalType":"uint256"},{"name":"elRewardsStealingFine","type":"uint256","internalType":"uint256"},{"name":"maxKeysPerOperatorEA","type":"uint256","internalType":"uint256"},{"name":"lidoLocator","type":"address","internalType":"address"}],"stateMutability":"nonpayable"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"EL_REWARDS_STEALING_FINE","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"INITIAL_SLASHING_PENALTY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"LIDO_LOCATOR","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ILidoLocator"}],"stateMutability":"view"},{"type":"function","name":"MAX_SIGNING_KEYS_PER_OPERATOR_BEFORE_PUBLIC_RELEASE","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"MODULE_MANAGER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_INFINITELY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RECOVERER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"REPORT_EL_REWARDS_STEALING_PENALTY_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RESUME_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"SETTLE_EL_REWARDS_STEALING_PENALTY_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"STAKING_ROUTER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"STETH","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IStETH"}],"stateMutability":"view"},{"type":"function","name":"VERIFIER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"accounting","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSAccounting"}],"stateMutability":"view"},{"type":"function","name":"activatePublicRelease","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"addNodeOperatorETH","inputs":[{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"},{"name":"managerAddress","type":"address","internalType":"address"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"eaProof","type":"bytes32[]","internalType":"bytes32[]"},{"name":"referrer","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"addNodeOperatorStETH","inputs":[{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"},{"name":"managerAddress","type":"address","internalType":"address"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]},{"name":"eaProof","type":"bytes32[]","internalType":"bytes32[]"},{"name":"referrer","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"addNodeOperatorWstETH","inputs":[{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"},{"name":"managerAddress","type":"address","internalType":"address"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]},{"name":"eaProof","type":"bytes32[]","internalType":"bytes32[]"},{"name":"referrer","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"addValidatorKeysETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"addValidatorKeysStETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"addValidatorKeysWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"cancelELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"claimRewardsStETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stETHAmount","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"claimRewardsUnstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stEthAmount","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"claimRewardsWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"wstETHAmount","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"cleanDepositQueue","inputs":[{"name":"maxItems","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"compensateELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"confirmNodeOperatorManagerAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"confirmNodeOperatorRewardAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"decreaseVettedSigningKeysCount","inputs":[{"name":"nodeOperatorIds","type":"bytes","internalType":"bytes"},{"name":"vettedSigningKeysCounts","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"depositETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"depositQueue","inputs":[],"outputs":[{"name":"head","type":"uint128","internalType":"uint128"},{"name":"length","type":"uint128","internalType":"uint128"}],"stateMutability":"view"},{"type":"function","name":"depositQueueItem","inputs":[{"name":"index","type":"uint128","internalType":"uint128"}],"outputs":[{"name":"","type":"uint256","internalType":"Batch"}],"stateMutability":"view"},{"type":"function","name":"depositStETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stETHAmount","type":"uint256","internalType":"uint256"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"depositWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"wstETHAmount","type":"uint256","internalType":"uint256"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"earlyAdoption","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSEarlyAdoption"}],"stateMutability":"view"},{"type":"function","name":"getActiveNodeOperatorsCount","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperator","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct NodeOperator","components":[{"name":"totalAddedKeys","type":"uint32","internalType":"uint32"},{"name":"totalWithdrawnKeys","type":"uint32","internalType":"uint32"},{"name":"totalDepositedKeys","type":"uint32","internalType":"uint32"},{"name":"totalVettedKeys","type":"uint32","internalType":"uint32"},{"name":"stuckValidatorsCount","type":"uint32","internalType":"uint32"},{"name":"depositableValidatorsCount","type":"uint32","internalType":"uint32"},{"name":"targetLimit","type":"uint32","internalType":"uint32"},{"name":"targetLimitMode","type":"uint8","internalType":"uint8"},{"name":"totalExitedKeys","type":"uint32","internalType":"uint32"},{"name":"enqueuedCount","type":"uint32","internalType":"uint32"},{"name":"managerAddress","type":"address","internalType":"address"},{"name":"proposedManagerAddress","type":"address","internalType":"address"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"proposedRewardAddress","type":"address","internalType":"address"}]}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorIds","inputs":[{"name":"offset","type":"uint256","internalType":"uint256"},{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"nodeOperatorIds","type":"uint256[]","internalType":"uint256[]"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorIsActive","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorNonWithdrawnKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorSummary","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"targetLimitMode","type":"uint256","internalType":"uint256"},{"name":"targetValidatorsCount","type":"uint256","internalType":"uint256"},{"name":"stuckValidatorsCount","type":"uint256","internalType":"uint256"},{"name":"refundedValidatorsCount","type":"uint256","internalType":"uint256"},{"name":"stuckPenaltyEndTimestamp","type":"uint256","internalType":"uint256"},{"name":"totalExitedValidators","type":"uint256","internalType":"uint256"},{"name":"totalDepositedValidators","type":"uint256","internalType":"uint256"},{"name":"depositableValidatorsCount","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorsCount","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNonce","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getResumeSinceTimestamp","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getSigningKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"startIndex","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bytes","internalType":"bytes"}],"stateMutability":"view"},{"type":"function","name":"getSigningKeysWithSignatures","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"startIndex","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"keys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"}],"stateMutability":"view"},{"type":"function","name":"getStakingModuleSummary","inputs":[],"outputs":[{"name":"totalExitedValidators","type":"uint256","internalType":"uint256"},{"name":"totalDepositedValidators","type":"uint256","internalType":"uint256"},{"name":"depositableValidatorsCount","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getType","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"_accounting","type":"address","internalType":"address"},{"name":"_earlyAdoption","type":"address","internalType":"address"},{"name":"_keyRemovalCharge","type":"uint256","internalType":"uint256"},{"name":"admin","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"isPaused","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"isValidatorSlashed","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"isValidatorWithdrawn","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"keyRemovalCharge","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"normalizeQueue","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"obtainDepositData","inputs":[{"name":"depositsCount","type":"uint256","internalType":"uint256"},{"name":"","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"}],"stateMutability":"nonpayable"},{"type":"function","name":"onExitedAndStuckValidatorsCountsUpdated","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"onRewardsMinted","inputs":[{"name":"totalShares","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"onWithdrawalCredentialsChanged","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"pauseFor","inputs":[{"name":"duration","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"proposeNodeOperatorManagerAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"proposedAddress","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"proposeNodeOperatorRewardAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"proposedAddress","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"publicRelease","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"recoverERC1155","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC20","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC721","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverEther","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverStETHShares","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"removeKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"startIndex","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"reportELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"blockHash","type":"bytes32","internalType":"bytes32"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resetNodeOperatorManagerAddress","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resume","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setKeyRemovalCharge","inputs":[{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"settleELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorIds","type":"uint256[]","internalType":"uint256[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"submitInitialSlashing","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"submitWithdrawal","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"unsafeUpdateValidatorsCount","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"exitedValidatorsKeysCount","type":"uint256","internalType":"uint256"},{"name":"stuckValidatorsKeysCount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateExitedValidatorsCount","inputs":[{"name":"nodeOperatorIds","type":"bytes","internalType":"bytes"},{"name":"exitedValidatorsCounts","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateRefundedValidatorsCount","inputs":[{"name":"","type":"uint256","internalType":"uint256"},{"name":"","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateStuckValidatorsCount","inputs":[{"name":"nodeOperatorIds","type":"bytes","internalType":"bytes"},{"name":"stuckValidatorsCounts","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateTargetValidatorsLimits","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"targetLimitMode","type":"uint256","internalType":"uint256"},{"name":"targetLimit","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"DepositedSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"depositedKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltyCancelled","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltyReported","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"proposedBlockHash","type":"bytes32","indexed":false,"internalType":"bytes32"},{"name":"stolenAmount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltySettled","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ExitedSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"exitedKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"InitialSlashingSubmitted","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"keyIndex","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"KeyRemovalChargeApplied","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"KeyRemovalChargeSet","inputs":[{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"NodeOperatorAdded","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"managerAddress","type":"address","indexed":true,"internalType":"address"},{"name":"rewardAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NonceChanged","inputs":[{"name":"nonce","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Paused","inputs":[{"name":"duration","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PublicRelease","inputs":[],"anonymous":false},{"type":"event","name":"ReferrerSet","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"referrer","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"Resumed","inputs":[],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"SigningKeyAdded","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"SigningKeyAdded","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"SigningKeyRemoved","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"SigningKeyRemoved","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"StuckSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"stuckKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"TargetValidatorsCountChangedByRequest","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"targetLimitMode","type":"uint8","indexed":false,"internalType":"uint8"},{"name":"targetValidatorsCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"TotalSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"totalKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"VettedSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"vettedKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"WithdrawalSubmitted","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"keyIndex","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"AlreadySet","inputs":[]},{"type":"error","name":"AlreadySubmitted","inputs":[]},{"type":"error","name":"AlreadyWithdrawn","inputs":[]},{"type":"error","name":"EmptyKey","inputs":[]},{"type":"error","name":"ExitedKeysDecrease","inputs":[]},{"type":"error","name":"ExitedKeysHigherThanTotalDeposited","inputs":[]},{"type":"error","name":"InvalidAmount","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidKeysCount","inputs":[]},{"type":"error","name":"InvalidLength","inputs":[]},{"type":"error","name":"InvalidReportData","inputs":[]},{"type":"error","name":"InvalidVetKeysPointer","inputs":[]},{"type":"error","name":"MaxSigningKeysCountExceeded","inputs":[]},{"type":"error","name":"NodeOperatorDoesNotExist","inputs":[]},{"type":"error","name":"NotAllowedToJoinYet","inputs":[]},{"type":"error","name":"NotAllowedToRecover","inputs":[]},{"type":"error","name":"NotEnoughKeys","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"NotSupported","inputs":[]},{"type":"error","name":"PauseUntilMustBeInFuture","inputs":[]},{"type":"error","name":"PausedExpected","inputs":[]},{"type":"error","name":"QueueIsEmpty","inputs":[]},{"type":"error","name":"ResumedExpected","inputs":[]},{"type":"error","name":"SenderIsNotEligible","inputs":[]},{"type":"error","name":"SigningKeysInvalidOffset","inputs":[]},{"type":"error","name":"StuckKeysHigherThanExited","inputs":[]},{"type":"error","name":"ZeroAccountingAddress","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroLocatorAddress","inputs":[]},{"type":"error","name":"ZeroPauseDuration","inputs":[]}] +[{"type":"constructor","inputs":[{"name":"moduleType","type":"bytes32","internalType":"bytes32"},{"name":"lidoLocator","type":"address","internalType":"address"},{"name":"parametersRegistry","type":"address","internalType":"address"}],"stateMutability":"nonpayable"},{"type":"function","name":"BAD_PERFORMER_EJECTOR_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"CREATE_NODE_OPERATOR_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"LIDO_LOCATOR","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ILidoLocator"}],"stateMutability":"view"},{"type":"function","name":"PARAMETERS_REGISTRY","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSParametersRegistry"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_INFINITELY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RECOVERER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"REPORT_EL_REWARDS_STEALING_PENALTY_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RESUME_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"SETTLE_EL_REWARDS_STEALING_PENALTY_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"SET_BOND_CURVE_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"STAKING_ROUTER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"STETH","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IStETH"}],"stateMutability":"view"},{"type":"function","name":"VERIFIER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"accounting","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSAccounting"}],"stateMutability":"view"},{"type":"function","name":"addValidatorKeysETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"addValidatorKeysStETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"addValidatorKeysWstETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"cancelELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"changeNodeOperatorRewardAddress","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"newAddress","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"claimRewardsStETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stETHAmount","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"claimedShares","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"claimRewardsUnstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stEthAmount","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"requestId","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"claimRewardsWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"wstETHAmount","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"claimedWstETHAmount","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"cleanDepositQueue","inputs":[{"name":"maxItems","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"removed","type":"uint256","internalType":"uint256"},{"name":"lastRemovedAtDepth","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"compensateELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"confirmNodeOperatorManagerAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"confirmNodeOperatorRewardAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"createNodeOperator","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"managementProperties","type":"tuple","internalType":"struct NodeOperatorManagementProperties","components":[{"name":"managerAddress","type":"address","internalType":"address"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"extendedManagerPermissions","type":"bool","internalType":"bool"}]},{"name":"referrer","type":"address","internalType":"address"}],"outputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"decreaseVettedSigningKeysCount","inputs":[{"name":"nodeOperatorIds","type":"bytes","internalType":"bytes"},{"name":"vettedSigningKeysCounts","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"depositETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"depositQueue","inputs":[],"outputs":[{"name":"head","type":"uint128","internalType":"uint128"},{"name":"tail","type":"uint128","internalType":"uint128"}],"stateMutability":"view"},{"type":"function","name":"depositQueueItem","inputs":[{"name":"index","type":"uint128","internalType":"uint128"}],"outputs":[{"name":"","type":"uint256","internalType":"Batch"}],"stateMutability":"view"},{"type":"function","name":"depositStETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stETHAmount","type":"uint256","internalType":"uint256"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"depositWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"wstETHAmount","type":"uint256","internalType":"uint256"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"ejectBadPerformer","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"},{"name":"strikesCount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"enqueueNodeOperatorKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"finalizeUpgradeV2","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"getActiveNodeOperatorsCount","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperator","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct NodeOperator","components":[{"name":"totalAddedKeys","type":"uint32","internalType":"uint32"},{"name":"totalWithdrawnKeys","type":"uint32","internalType":"uint32"},{"name":"totalDepositedKeys","type":"uint32","internalType":"uint32"},{"name":"totalVettedKeys","type":"uint32","internalType":"uint32"},{"name":"stuckValidatorsCount","type":"uint32","internalType":"uint32"},{"name":"depositableValidatorsCount","type":"uint32","internalType":"uint32"},{"name":"targetLimit","type":"uint32","internalType":"uint32"},{"name":"targetLimitMode","type":"uint8","internalType":"uint8"},{"name":"totalExitedKeys","type":"uint32","internalType":"uint32"},{"name":"enqueuedCount","type":"uint32","internalType":"uint32"},{"name":"managerAddress","type":"address","internalType":"address"},{"name":"proposedManagerAddress","type":"address","internalType":"address"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"proposedRewardAddress","type":"address","internalType":"address"},{"name":"extendedManagerPermissions","type":"bool","internalType":"bool"}]}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorIds","inputs":[{"name":"offset","type":"uint256","internalType":"uint256"},{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"nodeOperatorIds","type":"uint256[]","internalType":"uint256[]"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorIsActive","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorNonWithdrawnKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorSummary","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"targetLimitMode","type":"uint256","internalType":"uint256"},{"name":"targetValidatorsCount","type":"uint256","internalType":"uint256"},{"name":"stuckValidatorsCount","type":"uint256","internalType":"uint256"},{"name":"refundedValidatorsCount","type":"uint256","internalType":"uint256"},{"name":"stuckPenaltyEndTimestamp","type":"uint256","internalType":"uint256"},{"name":"totalExitedValidators","type":"uint256","internalType":"uint256"},{"name":"totalDepositedValidators","type":"uint256","internalType":"uint256"},{"name":"depositableValidatorsCount","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorsCount","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNonce","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getResumeSinceTimestamp","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getSigningKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"startIndex","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bytes","internalType":"bytes"}],"stateMutability":"view"},{"type":"function","name":"getSigningKeysWithSignatures","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"startIndex","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"keys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"}],"stateMutability":"view"},{"type":"function","name":"getStakingModuleSummary","inputs":[],"outputs":[{"name":"totalExitedValidators","type":"uint256","internalType":"uint256"},{"name":"totalDepositedValidators","type":"uint256","internalType":"uint256"},{"name":"depositableValidatorsCount","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getType","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"_accounting","type":"address","internalType":"address"},{"name":"admin","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"isPaused","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"isValidatorEjected","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"isValidatorWithdrawn","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"obtainDepositData","inputs":[{"name":"depositsCount","type":"uint256","internalType":"uint256"},{"name":"","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"}],"stateMutability":"nonpayable"},{"type":"function","name":"onExitedAndStuckValidatorsCountsUpdated","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"onRewardsMinted","inputs":[{"name":"totalShares","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"onWithdrawalCredentialsChanged","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"pauseFor","inputs":[{"name":"duration","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"proposeNodeOperatorManagerAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"proposedAddress","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"proposeNodeOperatorRewardAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"proposedAddress","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"publicRelease","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"recoverERC1155","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC20","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC721","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverEther","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"removeKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"startIndex","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"reportELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"blockHash","type":"bytes32","internalType":"bytes32"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resetNodeOperatorManagerAddress","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resume","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setBondCurve","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"settleELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorIds","type":"uint256[]","internalType":"uint256[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"submitWithdrawal","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"},{"name":"isSlashed","type":"bool","internalType":"bool"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"unsafeUpdateValidatorsCount","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"exitedValidatorsKeysCount","type":"uint256","internalType":"uint256"},{"name":"stuckValidatorsKeysCount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateExitedValidatorsCount","inputs":[{"name":"nodeOperatorIds","type":"bytes","internalType":"bytes"},{"name":"exitedValidatorsCounts","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateRefundedValidatorsCount","inputs":[{"name":"","type":"uint256","internalType":"uint256"},{"name":"","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"view"},{"type":"function","name":"updateStuckValidatorsCount","inputs":[{"name":"nodeOperatorIds","type":"bytes","internalType":"bytes"},{"name":"stuckValidatorsCounts","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateTargetValidatorsLimits","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"targetLimitMode","type":"uint256","internalType":"uint256"},{"name":"targetLimit","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"BatchEnqueued","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"count","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DepositableSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"depositableKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DepositedSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"depositedKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltyCancelled","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltyCompensated","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltyReported","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"proposedBlockHash","type":"bytes32","indexed":false,"internalType":"bytes32"},{"name":"stolenAmount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltySettled","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC1155Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC20Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC721Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"EjectionSubmitted","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"keyIndex","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"EtherRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ExitedSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"exitedKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"KeyRemovalChargeApplied","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"NodeOperatorAdded","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"managerAddress","type":"address","indexed":true,"internalType":"address"},{"name":"rewardAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NodeOperatorManagerAddressChangeProposed","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"oldProposedAddress","type":"address","indexed":true,"internalType":"address"},{"name":"newProposedAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NodeOperatorManagerAddressChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"oldAddress","type":"address","indexed":true,"internalType":"address"},{"name":"newAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NodeOperatorRewardAddressChangeProposed","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"oldProposedAddress","type":"address","indexed":true,"internalType":"address"},{"name":"newProposedAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NodeOperatorRewardAddressChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"oldAddress","type":"address","indexed":true,"internalType":"address"},{"name":"newAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NonceChanged","inputs":[{"name":"nonce","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Paused","inputs":[{"name":"duration","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ReferrerSet","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"referrer","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"Resumed","inputs":[],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"SigningKeyAdded","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"SigningKeyRemoved","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"StETHSharesRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"StuckSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"stuckKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"TargetValidatorsCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"targetLimitMode","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"targetValidatorsCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"TotalSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"totalKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"VettedSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"vettedKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"VettedSigningKeysCountDecreased","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"WithdrawalSubmitted","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"keyIndex","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"AlreadyEjected","inputs":[]},{"type":"error","name":"AlreadyProposed","inputs":[]},{"type":"error","name":"AlreadyWithdrawn","inputs":[]},{"type":"error","name":"EmptyKey","inputs":[]},{"type":"error","name":"ExitedKeysDecrease","inputs":[]},{"type":"error","name":"ExitedKeysHigherThanTotalDeposited","inputs":[]},{"type":"error","name":"FailedToSendEther","inputs":[]},{"type":"error","name":"InvalidAmount","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidInput","inputs":[]},{"type":"error","name":"InvalidKeysCount","inputs":[]},{"type":"error","name":"InvalidLength","inputs":[]},{"type":"error","name":"InvalidReportData","inputs":[]},{"type":"error","name":"InvalidVetKeysPointer","inputs":[]},{"type":"error","name":"MethodCallIsNotAllowed","inputs":[]},{"type":"error","name":"NodeOperatorDoesNotExist","inputs":[]},{"type":"error","name":"NodeOperatorHasKeys","inputs":[]},{"type":"error","name":"NotAllowedToRecover","inputs":[]},{"type":"error","name":"NotEnoughKeys","inputs":[]},{"type":"error","name":"NotEnoughStrikesToEject","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"NotSupported","inputs":[]},{"type":"error","name":"PauseUntilMustBeInFuture","inputs":[]},{"type":"error","name":"PausedExpected","inputs":[]},{"type":"error","name":"QueueIsEmpty","inputs":[]},{"type":"error","name":"QueueLookupNoLimit","inputs":[]},{"type":"error","name":"ResumedExpected","inputs":[]},{"type":"error","name":"SameAddress","inputs":[]},{"type":"error","name":"SenderIsNotEligible","inputs":[]},{"type":"error","name":"SenderIsNotManagerAddress","inputs":[]},{"type":"error","name":"SenderIsNotProposedAddress","inputs":[]},{"type":"error","name":"SenderIsNotRewardAddress","inputs":[]},{"type":"error","name":"SigningKeysInvalidOffset","inputs":[]},{"type":"error","name":"StuckKeysHigherThanNonExited","inputs":[]},{"type":"error","name":"ZeroAccountingAddress","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroLocatorAddress","inputs":[]},{"type":"error","name":"ZeroParametersRegistryAddress","inputs":[]},{"type":"error","name":"ZeroPauseDuration","inputs":[]},{"type":"error","name":"ZeroRewardAddress","inputs":[]},{"type":"error","name":"ZeroSenderAddress","inputs":[]}] \ No newline at end of file diff --git a/assets/CSParametersRegistry.json b/assets/CSParametersRegistry.json new file mode 100644 index 000000000..7db794b4f --- /dev/null +++ b/assets/CSParametersRegistry.json @@ -0,0 +1 @@ +[{"type":"constructor","inputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"defaultBadPerformancePenalty","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultElRewardsStealingAdditionalFine","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultKeyRemovalCharge","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultPerformanceCoefficients","inputs":[],"outputs":[{"name":"attestationsWeight","type":"uint32","internalType":"uint32"},{"name":"blocksWeight","type":"uint32","internalType":"uint32"},{"name":"syncWeight","type":"uint32","internalType":"uint32"}],"stateMutability":"view"},{"type":"function","name":"defaultPerformanceLeeway","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultPriorityQueueLimit","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultRewardShare","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultStrikesParams","inputs":[],"outputs":[{"name":"lifetime","type":"uint32","internalType":"uint32"},{"name":"threshold","type":"uint32","internalType":"uint32"}],"stateMutability":"view"},{"type":"function","name":"getBadPerformancePenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"penalty","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getElRewardsStealingAdditionalFine","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"fine","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getKeyRemovalCharge","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"keyRemovalCharge","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getPerformanceCoefficients","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"attestationsWeight","type":"uint256","internalType":"uint256"},{"name":"blocksWeight","type":"uint256","internalType":"uint256"},{"name":"syncWeight","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getPerformanceLeewayData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"keyPivots","type":"uint256[]","internalType":"uint256[]"},{"name":"performanceLeeways","type":"uint256[]","internalType":"uint256[]"}],"stateMutability":"view"},{"type":"function","name":"getPriorityQueueLimit","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"limit","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRewardShareData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"keyPivots","type":"uint256[]","internalType":"uint256[]"},{"name":"rewardShares","type":"uint256[]","internalType":"uint256[]"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getStrikesParams","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"lifetime","type":"uint256","internalType":"uint256"},{"name":"threshold","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"admin","type":"address","internalType":"address"},{"name":"data","type":"tuple","internalType":"struct ICSParametersRegistry.InitializationData","components":[{"name":"keyRemovalCharge","type":"uint256","internalType":"uint256"},{"name":"elRewardsStealingAdditionalFine","type":"uint256","internalType":"uint256"},{"name":"priorityQueueLimit","type":"uint256","internalType":"uint256"},{"name":"rewardShare","type":"uint256","internalType":"uint256"},{"name":"performanceLeeway","type":"uint256","internalType":"uint256"},{"name":"strikesLifetime","type":"uint256","internalType":"uint256"},{"name":"strikesThreshold","type":"uint256","internalType":"uint256"},{"name":"badPerformancePenalty","type":"uint256","internalType":"uint256"},{"name":"attestationsWeight","type":"uint256","internalType":"uint256"},{"name":"blocksWeight","type":"uint256","internalType":"uint256"},{"name":"syncWeight","type":"uint256","internalType":"uint256"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setBadPerformancePenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"penalty","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultBadPerformancePenalty","inputs":[{"name":"penalty","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultElRewardsStealingAdditionalFine","inputs":[{"name":"fine","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultKeyRemovalCharge","inputs":[{"name":"keyRemovalCharge","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultPerformanceCoefficients","inputs":[{"name":"attestationsWeight","type":"uint256","internalType":"uint256"},{"name":"blocksWeight","type":"uint256","internalType":"uint256"},{"name":"syncWeight","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultPerformanceLeeway","inputs":[{"name":"leeway","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultPriorityQueueLimit","inputs":[{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultRewardShare","inputs":[{"name":"share","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultStrikesParams","inputs":[{"name":"lifetime","type":"uint256","internalType":"uint256"},{"name":"threshold","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setElRewardsStealingAdditionalFine","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"fine","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setKeyRemovalCharge","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"keyRemovalCharge","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setPerformanceCoefficients","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"attestationsWeight","type":"uint256","internalType":"uint256"},{"name":"blocksWeight","type":"uint256","internalType":"uint256"},{"name":"syncWeight","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setPerformanceLeewayData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"keyPivots","type":"uint256[]","internalType":"uint256[]"},{"name":"performanceLeeways","type":"uint256[]","internalType":"uint256[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setPriorityQueueLimit","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setRewardShareData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"keyPivots","type":"uint256[]","internalType":"uint256[]"},{"name":"rewardShares","type":"uint256[]","internalType":"uint256[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setStrikesParams","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"lifetime","type":"uint256","internalType":"uint256"},{"name":"threshold","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"unsetBadPerformancePenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetElRewardsStealingAdditionalFine","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetKeyRemovalCharge","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetPerformanceCoefficients","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetPerformanceLeewayData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetPriorityQueueLimit","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetRewardShareData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetStrikesParams","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"BadPerformancePenaltySet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"penalty","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BadPerformancePenaltyUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultBadPerformancePenaltySet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultElRewardsStealingAdditionalFineSet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultKeyRemovalChargeSet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultPerformanceCoefficientsSet","inputs":[{"name":"attestationsWeight","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"blocksWeight","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"syncWeight","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultPerformanceLeewaySet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultPriorityQueueLimitSet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultRewardShareSet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultStrikesParamsSet","inputs":[{"name":"lifetime","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"threshold","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ElRewardsStealingAdditionalFineSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"fine","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ElRewardsStealingAdditionalFineUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"KeyRemovalChargeSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"keyRemovalCharge","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"KeyRemovalChargeUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PerformanceCoefficientsSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"attestationsWeight","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"blocksWeight","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"syncWeight","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PerformanceCoefficientsUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PerformanceLeewayDataSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PerformanceLeewayDataUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PriorityQueueLimitSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"limit","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PriorityQueueLimitUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"RewardShareDataSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"RewardShareDataUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"StrikesParamsSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"lifetime","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"threshold","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"StrikesParamsUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidPerformanceLeewayData","inputs":[]},{"type":"error","name":"InvalidRewardShareData","inputs":[]},{"type":"error","name":"InvalidStrikesParams","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"SafeCastOverflowedUintDowncast","inputs":[{"name":"bits","type":"uint8","internalType":"uint8"},{"name":"value","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"ZeroAdminAddress","inputs":[]}] \ No newline at end of file diff --git a/src/constants.py b/src/constants.py index f920bb0ea..1285398d3 100644 --- a/src/constants.py +++ b/src/constants.py @@ -46,3 +46,4 @@ GWEI_TO_WEI = 10**9 MAX_BLOCK_GAS_LIMIT = 30_000_000 UINT64_MAX = 2**64 - 1 +UINT256_MAX = 2**256 - 1 diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index 3a0ed5b87..9e0340466 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -1,10 +1,9 @@ import logging -from collections import defaultdict from typing import Iterator from hexbytes import HexBytes -from src.constants import TOTAL_BASIS_POINTS, UINT64_MAX +from src.constants import UINT64_MAX from src.metrics.prometheus.business import CONTRACT_ON_PAUSE from src.metrics.prometheus.csm import ( CSM_CURRENT_FRAME_RANGE_L_EPOCH, @@ -12,8 +11,9 @@ ) from src.metrics.prometheus.duration_meter import duration_meter from src.modules.csm.checkpoint import FrameCheckpointProcessor, FrameCheckpointsIterator, MinStepIsNotReached -from src.modules.csm.log import FramePerfLog, OperatorFrameSummary -from src.modules.csm.state import State, Frame, DutyAccumulator +from src.modules.csm.distribution import Distribution +from src.modules.csm.log import FramePerfLog +from src.modules.csm.state import State from src.modules.csm.tree import Tree from src.modules.csm.types import ReportData, Shares from src.modules.submodules.consensus import ConsensusModule @@ -27,14 +27,11 @@ EpochNumber, ReferenceBlockStamp, SlotNumber, - StakingModuleAddress, - StakingModuleId, ) -from src.utils.blockstamp import build_blockstamp from src.utils.cache import global_lru_cache as lru_cache -from src.utils.slot import get_next_non_missed_slot, get_reference_blockstamp +from src.utils.slot import get_reference_blockstamp from src.utils.web3converter import Web3Converter -from src.web3py.extensions.lido_validators import NodeOperatorId, StakingModule, ValidatorsByNodeOperator, LidoValidator +from src.web3py.extensions.lido_validators import NodeOperatorId, StakingModule from src.web3py.types import Web3 logger = logging.getLogger(__name__) @@ -58,8 +55,8 @@ class CSOracle(BaseModule, ConsensusModule): 2. Calculate the performance of each validator based on the attestations. 3. Calculate the share of each CSM node operator excluding underperforming validators. """ - - COMPATIBLE_ONCHAIN_VERSIONS = [(1, 1), (1, 2)] + # TODO: should be (Contract=2, Consensus=3) + COMPATIBLE_ONCHAIN_VERSIONS = [(2, 2)] report_contract: CSFeeOracleContract staking_module: StakingModule @@ -101,32 +98,36 @@ def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: if (prev_cid is None) != (prev_root == ZERO_HASH): raise InconsistentData(f"Got inconsistent previous tree data: {prev_root=} {prev_cid=}") - total_distributed, total_rewards, logs = self.calculate_distribution(blockstamp) + distribution = Distribution(self.w3, self.staking_module, self.converter(blockstamp), self.state) + total_distributed_rewards, total_rewards_map, total_rebate, logs = distribution.calculate(blockstamp) - if total_distributed != sum(total_rewards.values()): - raise InconsistentData(f"Invalid distribution: {sum(total_rewards.values())=} != {total_distributed=}") + if total_distributed_rewards != sum(total_rewards_map.values()): + raise InconsistentData(f"Invalid distribution: {sum(total_rewards_map.values())=} != {total_distributed_rewards=}") - log_cid = self.publish_log(logs) + logs_cid = self.publish_log(logs) - if not total_distributed and not total_rewards: - logger.info({"msg": "No rewards distributed in the current frame"}) + if not total_distributed_rewards and not total_rewards_map: + logger.info({"msg": f"No rewards distributed in the current frame. {total_rebate=}"}) return ReportData( self.get_consensus_version(blockstamp), blockstamp.ref_slot, tree_root=prev_root, tree_cid=prev_cid or "", - log_cid=log_cid, + log_cid=logs_cid, distributed=0, + rebate=total_rebate, + strikes_tree_root=ZERO_HASH, + strikes_tree_cid="", ).as_tuple() if prev_cid and prev_root != ZERO_HASH: # Update cumulative amount of stETH shares for all operators. for no_id, accumulated_rewards in self.get_accumulated_rewards(prev_cid, prev_root): - total_rewards[no_id] += accumulated_rewards + total_rewards_map[no_id] += accumulated_rewards else: logger.info({"msg": "No previous distribution. Nothing to accumulate"}) - tree = self.make_tree(total_rewards) + tree = self.make_tree(total_rewards_map) tree_cid = self.publish_tree(tree) return ReportData( @@ -134,8 +135,11 @@ def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: blockstamp.ref_slot, tree_root=tree.root, tree_cid=tree_cid, - log_cid=log_cid, - distributed=total_distributed, + log_cid=logs_cid, + distributed=total_distributed_rewards, + rebate=total_rebate, + strikes_tree_root=ZERO_HASH, + strikes_tree_cid="", ).as_tuple() def is_main_data_submitted(self, blockstamp: BlockStamp) -> bool: @@ -151,12 +155,6 @@ def is_reporting_allowed(self, blockstamp: ReferenceBlockStamp) -> bool: CONTRACT_ON_PAUSE.labels("csm").set(on_pause) return not on_pause - @lru_cache(maxsize=1) - def module_validators_by_node_operators(self, blockstamp: BlockStamp) -> ValidatorsByNodeOperator: - return self.w3.lido_validators.get_module_validators_by_node_operators( - StakingModuleAddress(self.w3.csm.module.address), blockstamp - ) - def validate_state(self, blockstamp: ReferenceBlockStamp) -> None: # NOTE: We cannot use `r_epoch` from the `current_frame_range` call because the `blockstamp` is a # `ReferenceBlockStamp`, hence it's a block the frame ends at. We use `ref_epoch` instead. @@ -225,168 +223,6 @@ def collect_data(self, blockstamp: BlockStamp) -> bool: return self.state.is_fulfilled - def calculate_distribution( - self, blockstamp: ReferenceBlockStamp - ) -> tuple[int, defaultdict[NodeOperatorId, int], list[FramePerfLog]]: - """Computes distribution of fee shares at the given timestamp""" - operators_to_validators = self.module_validators_by_node_operators(blockstamp) - - total_distributed = 0 - total_rewards = defaultdict[NodeOperatorId, int](int) - logs: list[FramePerfLog] = [] - - for frame in self.state.frames: - from_epoch, to_epoch = frame - logger.info({"msg": f"Calculating distribution for frame [{from_epoch};{to_epoch}]"}) - - frame_blockstamp = blockstamp - if to_epoch != blockstamp.ref_epoch: - frame_blockstamp = self._get_ref_blockstamp_for_frame(blockstamp, to_epoch) - - total_rewards_to_distribute = self.w3.csm.fee_distributor.shares_to_distribute(frame_blockstamp.block_hash) - rewards_to_distribute_in_frame = total_rewards_to_distribute - total_distributed - - rewards_in_frame, log = self._calculate_distribution_in_frame( - frame, frame_blockstamp, rewards_to_distribute_in_frame, operators_to_validators - ) - distributed_in_frame = sum(rewards_in_frame.values()) - - total_distributed += distributed_in_frame - if total_distributed > total_rewards_to_distribute: - raise CSMError(f"Invalid distribution: {total_distributed=} > {total_rewards_to_distribute=}") - - for no_id, rewards in rewards_in_frame.items(): - total_rewards[no_id] += rewards - - logs.append(log) - - return total_distributed, total_rewards, logs - - def _get_ref_blockstamp_for_frame( - self, blockstamp: ReferenceBlockStamp, frame_ref_epoch: EpochNumber - ) -> ReferenceBlockStamp: - converter = self.converter(blockstamp) - return get_reference_blockstamp( - cc=self.w3.cc, - ref_slot=converter.get_epoch_last_slot(frame_ref_epoch), - ref_epoch=frame_ref_epoch, - last_finalized_slot_number=blockstamp.slot_number, - ) - - def _calculate_distribution_in_frame( - self, - frame: Frame, - blockstamp: ReferenceBlockStamp, - rewards_to_distribute: int, - operators_to_validators: ValidatorsByNodeOperator - ): - threshold = self._get_performance_threshold(frame, blockstamp) - log = FramePerfLog(blockstamp, frame, threshold) - - participation_shares: defaultdict[NodeOperatorId, int] = defaultdict(int) - - stuck_operators = self.get_stuck_operators(frame, blockstamp) - for (_, no_id), validators in operators_to_validators.items(): - log_operator = log.operators[no_id] - if no_id in stuck_operators: - log_operator.stuck = True - continue - for validator in validators: - att_duty = self.state.att_data[frame].get(validator.index) - prop_duty = self.state.prop_data[frame].get(validator.index) - sync_duty = self.state.sync_data[frame].get(validator.index) - self.process_validator_duty( - validator, att_duty, prop_duty, sync_duty, threshold, participation_shares, log_operator - ) - - rewards_distribution = self.calc_rewards_distribution_in_frame(participation_shares, rewards_to_distribute) - - for no_id, no_rewards in rewards_distribution.items(): - log.operators[no_id].distributed = no_rewards - - log.distributable = rewards_to_distribute - - return rewards_distribution, log - - def _get_performance_threshold(self, frame: Frame, blockstamp: ReferenceBlockStamp) -> float: - att_perf = self.state.get_att_network_aggr(frame).perf - prop_perf = self.state.get_prop_network_aggr(frame).perf - sync_perf = self.state.get_sync_network_aggr(frame).perf - network_perf = 54 / 64 * att_perf + 8 / 64 * prop_perf + 2 / 64 * sync_perf - if network_perf > 1: - raise ValueError(f"Invalid network performance: {network_perf=}") - - perf_leeway = self.w3.csm.oracle.perf_leeway_bp(blockstamp.block_hash) / TOTAL_BASIS_POINTS - threshold = network_perf - perf_leeway - return threshold - - @staticmethod - def process_validator_duty( - validator: LidoValidator, - attestation_duty: DutyAccumulator | None, - sync_duty: DutyAccumulator | None, - proposal_duty: DutyAccumulator | None, - threshold: float, - participation_shares: defaultdict[NodeOperatorId, int], - log_operator: OperatorFrameSummary - ): - if attestation_duty is None: - # It's possible that the validator is not assigned to any duty, hence it's performance - # is not presented in the aggregates (e.g. exited, pending for activation etc). - # TODO: check `sync_aggr` to strike (in case of bad sync performance) after validator exit - return - - log_validator = log_operator.validators[validator.index] - - if validator.validator.slashed is True: - # It means that validator was active during the frame and got slashed and didn't meet the exit - # epoch, so we should not count such validator for operator's share. - log_validator.slashed = True - return - - performance = CSOracle._calculate_validator_performance(attestation_duty, proposal_duty, sync_duty) - - if performance > threshold: - # Count of assigned attestations used as a metrics of time - # the validator was active in the current frame. - participation_shares[validator.lido_id.operatorIndex] += attestation_duty.assigned - - log_validator.performance = performance - log_validator.attestation_duty = attestation_duty - if proposal_duty: - log_validator.proposal_duty = proposal_duty - if sync_duty: - log_validator.sync_duty = sync_duty - - @staticmethod - def _calculate_validator_performance(att_aggr, prop_aggr, sync_aggr) -> float: - performance = att_aggr.perf - if prop_aggr and sync_aggr: - performance = 54 / 64 * att_aggr.perf + 8 / 64 * prop_aggr.perf + 2 / 64 * sync_aggr.perf - elif prop_aggr: - performance = 54 / 62 * att_aggr.perf + 8 / 62 * prop_aggr.perf - elif sync_aggr: - performance = 54 / 56 * att_aggr.perf + 2 / 56 * sync_aggr.perf - if performance > 1: - raise ValueError(f"Invalid performance: {performance=}") - return performance - - @staticmethod - def calc_rewards_distribution_in_frame( - participation_shares: dict[NodeOperatorId, int], - rewards_to_distribute: int, - ) -> dict[NodeOperatorId, int]: - rewards_distribution: dict[NodeOperatorId, int] = defaultdict(int) - total_participation = sum(participation_shares.values()) - - for no_id, no_participation_share in participation_shares.items(): - if no_participation_share == 0: - # Skip operators with zero participation - continue - rewards_distribution[no_id] = rewards_to_distribute * no_participation_share // total_participation - - return rewards_distribution - def get_accumulated_rewards(self, cid: CID, root: HexBytes) -> Iterator[tuple[NodeOperatorId, Shares]]: logger.info({"msg": "Fetching tree by CID from IPFS", "cid": repr(cid)}) tree = Tree.decode(self.w3.ipfs.fetch(cid)) @@ -399,31 +235,6 @@ def get_accumulated_rewards(self, cid: CID, root: HexBytes) -> Iterator[tuple[No for v in tree.tree.values: yield v["value"] - def get_stuck_operators(self, frame: Frame, frame_blockstamp: ReferenceBlockStamp) -> set[NodeOperatorId]: - l_epoch, _ = frame - l_ref_slot = self.converter(frame_blockstamp).get_epoch_first_slot(l_epoch) - # NOTE: r_block is guaranteed to be <= ref_slot, and the check - # in the inner frames assures the l_block <= r_block. - l_blockstamp = build_blockstamp( - get_next_non_missed_slot( - self.w3.cc, - l_ref_slot, - frame_blockstamp.slot_number, - ) - ) - - digests = self.w3.lido_contracts.staking_router.get_all_node_operator_digests( - self.staking_module, l_blockstamp.block_hash - ) - if not digests: - logger.warning("No CSM digest at blockstamp=%s, module was not added yet?", l_blockstamp) - stuck_from_digests = (no.id for no in digests if no.stuck_validators_count > 0) - stuck_from_events = self.w3.csm.get_operators_with_stucks_in_range( - l_blockstamp.block_hash, - frame_blockstamp.block_hash, - ) - return set(stuck_from_digests) | set(stuck_from_events) - def make_tree(self, shares: dict[NodeOperatorId, Shares]) -> Tree: if not shares: raise ValueError("No shares to build a tree") diff --git a/src/modules/csm/distribution.py b/src/modules/csm/distribution.py new file mode 100644 index 000000000..8c961fbdf --- /dev/null +++ b/src/modules/csm/distribution.py @@ -0,0 +1,286 @@ +import logging +from collections import defaultdict +from functools import lru_cache + +from src.constants import UINT256_MAX, TOTAL_BASIS_POINTS +from src.modules.csm.log import FramePerfLog, OperatorFrameSummary +from src.modules.csm.state import DutyAccumulator, Frame, State +from src.providers.execution.contracts.cs_parameters_registry import PerformanceCoefficients +from src.types import NodeOperatorId, ReferenceBlockStamp, EpochNumber, StakingModuleAddress +from src.utils.blockstamp import build_blockstamp +from src.utils.slot import get_reference_blockstamp, get_next_non_missed_slot +from src.utils.web3converter import Web3Converter +from src.web3py.extensions.lido_validators import LidoValidator, ValidatorsByNodeOperator, StakingModule +from src.web3py.types import Web3 + +logger = logging.getLogger(__name__) + + +class Distribution: + + w3: Web3 + staking_module: StakingModule + converter: Web3Converter + state: State + + def __init__(self, w3: Web3, staking_module: StakingModule, converter: Web3Converter, state: State): + self.w3 = w3 + self.staking_module = staking_module + self.converter = converter + self.state = state + + def calculate( + self, blockstamp: ReferenceBlockStamp + ) -> tuple[int, defaultdict[NodeOperatorId, int], int, list[FramePerfLog]]: + """Computes distribution of fee shares at the given timestamp""" + total_distributed_rewards = 0 + total_rewards_map = defaultdict[NodeOperatorId, int](int) + total_rebate = 0 + logs: list[FramePerfLog] = [] + + for frame in self.state.frames: + logger.info({"msg": f"Calculating distribution for {frame=}"}) + from_epoch, to_epoch = frame + frame_blockstamp = self._get_frame_blockstamp(blockstamp, to_epoch) + frame_module_validators = self._get_module_validators(frame_blockstamp) + + total_rewards_to_distribute = self.w3.csm.fee_distributor.shares_to_distribute(frame_blockstamp.block_hash) + rewards_to_distribute_in_frame = total_rewards_to_distribute - (total_distributed_rewards + total_rebate) + + rewards_in_frame, log = self._calculate_distribution_in_frame( + frame, frame_blockstamp, rewards_to_distribute_in_frame, frame_module_validators + ) + + total_distributed_rewards += log.distributed_rewards + total_rebate += log.rebate_to_protocol + + self.validate_distribution(total_distributed_rewards, total_rebate, total_rewards_to_distribute) + + for no_id, rewards in rewards_in_frame.items(): + total_rewards_map[no_id] += rewards + + logs.append(log) + + return total_distributed_rewards, total_rewards_map, total_rebate, logs + + def _get_frame_blockstamp(self, blockstamp: ReferenceBlockStamp, to_epoch: EpochNumber) -> ReferenceBlockStamp: + if to_epoch != blockstamp.ref_epoch: + return self._get_ref_blockstamp_for_frame(blockstamp, to_epoch) + return blockstamp + + def _get_ref_blockstamp_for_frame( + self, blockstamp: ReferenceBlockStamp, frame_ref_epoch: EpochNumber + ) -> ReferenceBlockStamp: + return get_reference_blockstamp( + cc=self.w3.cc, + ref_slot=self.converter.get_epoch_last_slot(frame_ref_epoch), + ref_epoch=frame_ref_epoch, + last_finalized_slot_number=blockstamp.slot_number, + ) + + def _get_module_validators(self, blockstamp: ReferenceBlockStamp) -> ValidatorsByNodeOperator: + return self.w3.lido_validators.get_module_validators_by_node_operators( + StakingModuleAddress(self.w3.csm.module.address), blockstamp + ) + + def _calculate_distribution_in_frame( + self, + frame: Frame, + blockstamp: ReferenceBlockStamp, + rewards_to_distribute: int, + operators_to_validators: ValidatorsByNodeOperator + ) -> tuple[dict[NodeOperatorId, int], FramePerfLog]: + total_rebate_share = 0 + participation_shares: defaultdict[NodeOperatorId, int] = defaultdict(int) + log = FramePerfLog(blockstamp, frame) + + network_perf = self._get_network_performance(frame) + + stuck_operators = self._get_stuck_operators(frame, blockstamp) + for (_, no_id), validators in operators_to_validators.items(): + logger.info({"msg": f"Calculating distribution for {no_id=}"}) + log_operator = log.operators[no_id] + if no_id in stuck_operators: + log_operator.stuck = True + continue + + curve_id = self.w3.csm.accounting.get_bond_curve_id(no_id, blockstamp.block_hash) + perf_coeffs, perf_leeway_data, reward_share_data = self._get_curve_params(curve_id, blockstamp) + + sorted_validators = sorted(validators, key=lambda v: v.index) + for number, validator in enumerate(sorted_validators): + att_duty = self.state.att_data[frame].get(validator.index) + prop_duty = self.state.prop_data[frame].get(validator.index) + sync_duty = self.state.sync_data[frame].get(validator.index) + + if att_duty is None: + # It's possible that the validator is not assigned to any duty, hence it's performance + # is not presented in the aggregates (e.g. exited, pending for activation etc). + # TODO: check `sync_aggr` to strike (in case of bad sync performance) after validator exit + continue + + threshold = None + reward_share = None + + # FIXME: CHECK default returns [] for key_pivots or [0] ??? + perf_leeway_data.key_pivots.append(UINT256_MAX) + for i, pivot_number in enumerate(perf_leeway_data.key_pivots): + if number <= pivot_number: + threshold = network_perf - perf_leeway_data.performance_leeways[i] / TOTAL_BASIS_POINTS + break + + reward_share_data.key_pivots.append(UINT256_MAX) + for i, pivot_number in enumerate(reward_share_data.key_pivots): + if number <= pivot_number: + reward_share = reward_share_data.reward_shares[i] / TOTAL_BASIS_POINTS + break + + if threshold is None or reward_share is None: + raise ValueError(f"Failed to calculate threshold or reward share for {validator.index=}") + + validator_rebate = self.process_validator_duty( + validator, + att_duty, + prop_duty, + sync_duty, + threshold, + reward_share, + perf_coeffs, + participation_shares, + log_operator + ) + total_rebate_share += validator_rebate + + rewards_distribution = self.calc_rewards_distribution_in_frame( + participation_shares, + total_rebate_share, + rewards_to_distribute + ) + + for no_id, no_rewards in rewards_distribution.items(): + log.operators[no_id].distributed = no_rewards + + log.distributable = rewards_to_distribute + log.distributed_rewards = sum(rewards_distribution.values()) + log.rebate_to_protocol = rewards_to_distribute - log.distributed_rewards + + return rewards_distribution, log + + @lru_cache(maxsize=32) # TODO: any feasible size regard to curves count + def _get_curve_params(self, curve_id: int, blockstamp: ReferenceBlockStamp): + perf_coeffs = self.w3.csm.params.get_performance_coefficients(curve_id, blockstamp.block_hash) + perf_leeway_data = self.w3.csm.params.get_performance_leeway_data(curve_id, blockstamp.block_hash) + reward_share_data = self.w3.csm.params.get_reward_share_data(curve_id, blockstamp.block_hash) + return perf_coeffs, perf_leeway_data, reward_share_data + + def _get_network_performance(self, frame: Frame) -> float: + att_perf = self.state.get_att_network_aggr(frame) + prop_perf = self.state.get_prop_network_aggr(frame) + sync_perf = self.state.get_sync_network_aggr(frame) + network_perf_coeffs = PerformanceCoefficients(attestations_weight=54, blocks_weight=8, sync_weight=2) + network_perf = Distribution.calculate_performance(att_perf, prop_perf, sync_perf, network_perf_coeffs) + return network_perf + + def _get_stuck_operators(self, frame: Frame, frame_blockstamp: ReferenceBlockStamp) -> set[NodeOperatorId]: + l_epoch, _ = frame + l_ref_slot = self.converter.get_epoch_first_slot(l_epoch) + # NOTE: r_block is guaranteed to be <= ref_slot, and the check + # in the inner frames assures the l_block <= r_block. + l_blockstamp = build_blockstamp( + get_next_non_missed_slot( + self.w3.cc, + l_ref_slot, + frame_blockstamp.slot_number, + ) + ) + + digests = self.w3.lido_contracts.staking_router.get_all_node_operator_digests( + self.staking_module, l_blockstamp.block_hash + ) + if not digests: + logger.warning("No CSM digest at blockstamp=%s, module was not added yet?", l_blockstamp) + stuck_from_digests = (no.id for no in digests if no.stuck_validators_count > 0) + stuck_from_events = self.w3.csm.get_operators_with_stucks_in_range( + l_blockstamp.block_hash, + frame_blockstamp.block_hash, + ) + return set(stuck_from_digests) | set(stuck_from_events) + + @staticmethod + def process_validator_duty( + validator: LidoValidator, + attestation_duty: DutyAccumulator | None, + sync_duty: DutyAccumulator | None, + proposal_duty: DutyAccumulator | None, + threshold: float, + reward_share: float, + performance_coefficients: PerformanceCoefficients, + participation_shares: defaultdict[NodeOperatorId, int], + log_operator: OperatorFrameSummary + ) -> int: + log_validator = log_operator.validators[validator.index] + + log_validator.threshold = threshold + log_validator.rewards_share = reward_share + + if validator.validator.slashed is True: + # It means that validator was active during the frame and got slashed and didn't meet the exit + # epoch, so we should not count such validator for operator's share. + log_validator.slashed = True + return 0 + + performance = Distribution.calculate_performance(attestation_duty, proposal_duty, sync_duty, performance_coefficients) + + log_validator.performance = performance + log_validator.attestation_duty = attestation_duty + log_validator.proposal_duty = proposal_duty + log_validator.sync_duty = sync_duty + + if performance > threshold: + # Count of assigned attestations used as a metrics of time the validator was active in the current frame. + # Reward share is a share of the operator's reward the validator should get. It can be less than 1. + participation_share = max(int(attestation_duty.assigned * reward_share), 1) + participation_shares[validator.lido_id.operatorIndex] += participation_share + rebate = attestation_duty.assigned - participation_share + return rebate + + return 0 + + @staticmethod + def calculate_performance(att_aggr, prop_aggr, sync_aggr, coeffs) -> float: + performance = att_aggr.perf + if prop_aggr and sync_aggr: + base = coeffs.attestations_weight + coeffs.blocks_weight + coeffs.sync_weight + performance = coeffs.attestations_weight / base * att_aggr.perf + coeffs.blocks_weight / base * prop_aggr.perf + coeffs.sync_weight / base * sync_aggr.perf + elif prop_aggr: + base = coeffs.attestations_weight + coeffs.blocks_weight + performance = coeffs.attestations_weight / base * att_aggr.perf + coeffs.blocks_weight / base * prop_aggr.perf + elif sync_aggr: + base = coeffs.attestations_weight + coeffs.sync_weight + performance = coeffs.attestations_weight / base * att_aggr.perf + coeffs.sync_weight / base * sync_aggr.perf + if performance > 1: + raise ValueError(f"Invalid performance: {performance=}") + return performance + + @staticmethod + def calc_rewards_distribution_in_frame( + participation_shares: dict[NodeOperatorId, int], + rebate_share: int, + rewards_to_distribute: int, + ) -> dict[NodeOperatorId, int]: + rewards_distribution: dict[NodeOperatorId, int] = defaultdict(int) + total_shares = rebate_share + sum(participation_shares.values()) + + for no_id, no_participation_share in participation_shares.items(): + if no_participation_share == 0: + # Skip operators with zero participation + continue + rewards_distribution[no_id] = rewards_to_distribute * no_participation_share // total_shares + + return rewards_distribution + + @staticmethod + def validate_distribution(total_distributed_rewards, total_rebate, total_rewards_to_distribute): + if (total_distributed_rewards + total_rebate) > total_rewards_to_distribute: + raise ValueError( + f"Invalid distribution: {total_distributed_rewards + total_rebate} > {total_rewards_to_distribute}") diff --git a/src/modules/csm/log.py b/src/modules/csm/log.py index 572bfbbee..85d9caa68 100644 --- a/src/modules/csm/log.py +++ b/src/modules/csm/log.py @@ -4,36 +4,42 @@ from src.modules.csm.state import DutyAccumulator from src.modules.csm.types import Shares +from src.providers.execution.contracts.cs_parameters_registry import PerformanceCoefficients from src.types import EpochNumber, NodeOperatorId, ReferenceBlockStamp, ValidatorIndex class LogJSONEncoder(json.JSONEncoder): ... +# TODO: Should we log params? + @dataclass class ValidatorFrameSummary: + performance: float = 0.0 + threshold: float = 0.0 + rewards_share: float = 0.0 + slashed: bool = False attestation_duty: DutyAccumulator = field(default_factory=DutyAccumulator) proposal_duty: DutyAccumulator = field(default_factory=DutyAccumulator) sync_duty: DutyAccumulator = field(default_factory=DutyAccumulator) - performance: float = 0.0 - slashed: bool = False @dataclass class OperatorFrameSummary: distributed: int = 0 - validators: dict[ValidatorIndex, ValidatorFrameSummary] = field(default_factory=lambda: defaultdict(ValidatorFrameSummary)) stuck: bool = False + performance_coefficients: PerformanceCoefficients = None + validators: dict[ValidatorIndex, ValidatorFrameSummary] = field(default_factory=lambda: defaultdict(ValidatorFrameSummary)) @dataclass class FramePerfLog: """A log of performance assessed per operator in the given frame""" - blockstamp: ReferenceBlockStamp frame: tuple[EpochNumber, EpochNumber] - threshold: float = 0.0 distributable: Shares = 0 + distributed_rewards: Shares = 0 + rebate_to_protocol: Shares = 0 operators: dict[NodeOperatorId, OperatorFrameSummary] = field( default_factory=lambda: defaultdict(OperatorFrameSummary) ) diff --git a/src/modules/csm/types.py b/src/modules/csm/types.py index 541a0fc5f..57cb0e5ba 100644 --- a/src/modules/csm/types.py +++ b/src/modules/csm/types.py @@ -22,6 +22,9 @@ class ReportData: tree_cid: CID | Literal[""] log_cid: CID distributed: int + rebate: int + strikes_tree_root: HexBytes + strikes_tree_cid: CID | Literal[""] def as_tuple(self): # Tuple with report in correct order @@ -32,4 +35,7 @@ def as_tuple(self): str(self.tree_cid), str(self.log_cid), self.distributed, + self.rebate, + self.strikes_tree_root, + str(self.strikes_tree_cid), ) diff --git a/src/providers/execution/contracts/cs_accounting.py b/src/providers/execution/contracts/cs_accounting.py index 786ea7f27..8a1000be3 100644 --- a/src/providers/execution/contracts/cs_accounting.py +++ b/src/providers/execution/contracts/cs_accounting.py @@ -4,6 +4,7 @@ from web3 import Web3 from web3.types import BlockIdentifier +from src.types import NodeOperatorId from ..base_interface import ContractInterface logger = logging.getLogger(__name__) @@ -24,3 +25,16 @@ def fee_distributor(self, block_identifier: BlockIdentifier = "latest") -> Check } ) return Web3.to_checksum_address(resp) + + def get_bond_curve_id(self, node_operator_id: NodeOperatorId, block_identifier: BlockIdentifier = "latest") -> int: + """Returns the curve ID""" + + resp = self.functions.getBondCurveId(node_operator_id).call(block_identifier=block_identifier) + logger.info( + { + "msg": f"Call `getBondCurveId({node_operator_id})`.", + "value": resp, + "block_identifier": repr(block_identifier), + } + ) + return resp diff --git a/src/providers/execution/contracts/cs_module.py b/src/providers/execution/contracts/cs_module.py index 2b0b37e29..3b1558a5e 100644 --- a/src/providers/execution/contracts/cs_module.py +++ b/src/providers/execution/contracts/cs_module.py @@ -43,6 +43,19 @@ def accounting(self, block_identifier: BlockIdentifier = "latest") -> ChecksumAd ) return Web3.to_checksum_address(resp) + def parameters_registry(self, block_identifier: BlockIdentifier = "latest") -> ChecksumAddress: + """Returns the address of the CSParametersRegistry contract""" + + resp = self.functions.PARAMETERS_REGISTRY().call(block_identifier=block_identifier) + logger.info( + { + "msg": "Call `PARAMETERS_REGISTRY()`.", + "value": resp, + "block_identifier": repr(block_identifier), + } + ) + return Web3.to_checksum_address(resp) + def is_paused(self, block: BlockIdentifier = "latest") -> bool: resp = self.functions.isPaused().call(block_identifier=block) logger.info( diff --git a/src/providers/execution/contracts/cs_parameters_registry.py b/src/providers/execution/contracts/cs_parameters_registry.py new file mode 100644 index 000000000..89b910614 --- /dev/null +++ b/src/providers/execution/contracts/cs_parameters_registry.py @@ -0,0 +1,82 @@ +import logging +from dataclasses import dataclass + +from web3.types import BlockIdentifier + +from src.providers.execution.base_interface import ContractInterface + +logger = logging.getLogger(__name__) + + +@dataclass +class PerformanceCoefficients: + attestations_weight: int + blocks_weight: int + sync_weight: int + + +@dataclass +class RewardShare: + key_pivots: list[int] + reward_shares: list[int] + + +@dataclass +class PerformanceLeeway: + key_pivots: list[int] + performance_leeways: list[int] + + +class CSParametersRegistryContract(ContractInterface): + abi_path = "./assets/CSParametersRegistry.json" + + def get_performance_coefficients( + self, + curve_id: int, + block_identifier: BlockIdentifier = "latest" + ) -> PerformanceCoefficients: + """Returns performance coefficients for given node operator""" + + resp = self.functions.getPerformanceCoefficients(curve_id).call(block_identifier=block_identifier) + logger.info( + { + "msg": f"Call `getPerformanceCoefficients({curve_id})`.", + "value": resp, + "block_identifier": repr(block_identifier), + } + ) + return PerformanceCoefficients(*resp) + + def get_reward_share_data( + self, + curve_id: int, + block_identifier: BlockIdentifier = "latest" + ) -> RewardShare: + """Returns reward share data for given node operator""" + + resp = self.functions.getRewardShareData(curve_id).call(block_identifier=block_identifier) + logger.info( + { + "msg": f"Call `getRewardShareData({curve_id})`.", + "value": resp, + "block_identifier": repr(block_identifier), + } + ) + return RewardShare(*resp) + + def get_performance_leeway_data( + self, + curve_id: int, + block_identifier: BlockIdentifier = "latest" + ) -> PerformanceLeeway: + """Returns performance leeway data for given node operator""" + + resp = self.functions.getPerformanceLeewayData(curve_id).call(block_identifier=block_identifier) + logger.info( + { + "msg": f"Call `getPerformanceLeewayData({curve_id})`.", + "value": resp, + "block_identifier": repr(block_identifier), + } + ) + return PerformanceLeeway(*resp) \ No newline at end of file diff --git a/src/web3py/extensions/csm.py b/src/web3py/extensions/csm.py index 65206cda7..2f80a2b39 100644 --- a/src/web3py/extensions/csm.py +++ b/src/web3py/extensions/csm.py @@ -19,6 +19,7 @@ from src.providers.execution.contracts.cs_fee_distributor import CSFeeDistributorContract from src.providers.execution.contracts.cs_fee_oracle import CSFeeOracleContract from src.providers.execution.contracts.cs_module import CSModuleContract +from src.providers.execution.contracts.cs_parameters_registry import CSParametersRegistryContract from src.providers.ipfs import CID, CIDv0, CIDv1, is_cid_v0 from src.types import BlockStamp, SlotNumber from src.utils.events import get_events_in_range @@ -31,8 +32,10 @@ class CSM(Module): w3: Web3 oracle: CSFeeOracleContract + accounting: CSAccountingContract fee_distributor: CSFeeDistributorContract module: CSModuleContract + params: CSParametersRegistryContract def __init__(self, w3: Web3) -> None: super().__init__(w3) @@ -88,7 +91,16 @@ def _load_contracts(self) -> None: ), ) - accounting = cast( + self.params = cast( + CSParametersRegistryContract, + self.w3.eth.contract( + address=self.module.parameters_registry(), + ContractFactoryClass=CSParametersRegistryContract, + decode_tuples=True, + ), + ) + + self.accounting = cast( CSAccountingContract, self.w3.eth.contract( address=self.module.accounting(), @@ -100,7 +112,7 @@ def _load_contracts(self) -> None: self.fee_distributor = cast( CSFeeDistributorContract, self.w3.eth.contract( - address=accounting.fee_distributor(), + address=self.accounting.fee_distributor(), ContractFactoryClass=CSFeeDistributorContract, decode_tuples=True, ), From 6d0ab0148757ca1f0d89cfcf3c5f9310d1846760 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Wed, 19 Feb 2025 11:08:33 +0100 Subject: [PATCH 016/162] fix: _calculate_frames --- src/modules/csm/state.py | 8 ++++---- tests/modules/csm/test_state.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/modules/csm/state.py b/src/modules/csm/state.py index 46f664157..0cb3eeac8 100644 --- a/src/modules/csm/state.py +++ b/src/modules/csm/state.py @@ -112,14 +112,14 @@ def is_fulfilled(self) -> bool: @property def frames(self): - return self.calculate_frames(self._epochs_to_process, self._epochs_per_frame) + return self._calculate_frames(self._epochs_to_process, self._epochs_per_frame) @staticmethod - def calculate_frames(epochs_to_process: tuple[EpochNumber, ...], epochs_per_frame: int) -> list[Frame]: + def _calculate_frames(epochs_to_process: tuple[EpochNumber, ...], epochs_per_frame: int) -> list[Frame]: """Split epochs to process into frames of `epochs_per_frame` length""" if len(epochs_to_process) % epochs_per_frame != 0: raise ValueError("Insufficient epochs to form a frame") - return [(frame[0], frame[-1]) for frame in batched(epochs_to_process, epochs_per_frame)] + return [(frame[0], frame[-1]) for frame in batched(sorted(epochs_to_process), epochs_per_frame)] def clear(self) -> None: self.data = {} @@ -156,7 +156,7 @@ def init_or_migrate( ) self.clear() - frames = self.calculate_frames(tuple(sequence(l_epoch, r_epoch)), epochs_per_frame) + frames = self._calculate_frames(tuple(sequence(l_epoch, r_epoch)), epochs_per_frame) frames_data: StateData = {frame: defaultdict(AttestationsAccumulator) for frame in frames} if not self.is_empty: diff --git a/tests/modules/csm/test_state.py b/tests/modules/csm/test_state.py index ee09fe1e5..82763561a 100644 --- a/tests/modules/csm/test_state.py +++ b/tests/modules/csm/test_state.py @@ -113,14 +113,14 @@ def test_is_fulfilled_returns_false_if_unprocessed_epochs_exist(): def test_calculate_frames_handles_exact_frame_size(): epochs = tuple(range(10)) - frames = State.calculate_frames(epochs, 5) + frames = State._calculate_frames(epochs, 5) assert frames == [(0, 4), (5, 9)] def test_calculate_frames_raises_error_for_insufficient_epochs(): epochs = tuple(range(8)) with pytest.raises(ValueError, match="Insufficient epochs to form a frame"): - State.calculate_frames(epochs, 5) + State._calculate_frames(epochs, 5) def test_clear_resets_state_to_empty(): From 9ebdc412b9709e60fc0c4bb3094ff0f28b0afb82 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Wed, 19 Feb 2025 11:23:56 +0100 Subject: [PATCH 017/162] feat: cached `find_frame` --- src/modules/csm/checkpoint.py | 3 +-- src/modules/csm/state.py | 8 +++++--- tests/modules/csm/test_state.py | 21 ++++++++++++++++----- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/modules/csm/checkpoint.py b/src/modules/csm/checkpoint.py index 69d0a79dd..222d1e626 100644 --- a/src/modules/csm/checkpoint.py +++ b/src/modules/csm/checkpoint.py @@ -205,12 +205,11 @@ def _check_duty( for root in block_roots: attestations = self.cc.get_block_attestations(root) process_attestations(attestations, committees, self.eip7549_supported) - frame = self.state.find_frame(duty_epoch) with lock: for committee in committees.values(): for validator_duty in committee: self.state.increment_duty( - frame, + duty_epoch, validator_duty.index, included=validator_duty.included, ) diff --git a/src/modules/csm/state.py b/src/modules/csm/state.py index 0cb3eeac8..6ea3db1a2 100644 --- a/src/modules/csm/state.py +++ b/src/modules/csm/state.py @@ -3,6 +3,7 @@ import pickle from collections import defaultdict from dataclasses import dataclass +from functools import lru_cache from itertools import batched from pathlib import Path from typing import Self @@ -127,15 +128,15 @@ def clear(self) -> None: self._processed_epochs.clear() assert self.is_empty + @lru_cache(variables.CSM_ORACLE_MAX_CONCURRENCY) def find_frame(self, epoch: EpochNumber) -> Frame: for epoch_range in self.frames: if epoch_range[0] <= epoch <= epoch_range[1]: return epoch_range raise ValueError(f"Epoch {epoch} is out of frames range: {self.frames}") - def increment_duty(self, frame: Frame, val_index: ValidatorIndex, included: bool) -> None: - if frame not in self.data: - raise ValueError(f"Frame {frame} is not found in the state") + def increment_duty(self, epoch: EpochNumber, val_index: ValidatorIndex, included: bool) -> None: + frame = self.find_frame(epoch) self.data[frame][val_index].add_duty(included) def add_processed_epoch(self, epoch: EpochNumber) -> None: @@ -176,6 +177,7 @@ def init_or_migrate( self._epochs_per_frame = epochs_per_frame self._epochs_to_process = tuple(sequence(l_epoch, r_epoch)) self._consensus_version = consensus_version + self.find_frame.cache_clear() self.commit() def _migrate_frames_data( diff --git a/tests/modules/csm/test_state.py b/tests/modules/csm/test_state.py index 82763561a..97004053c 100644 --- a/tests/modules/csm/test_state.py +++ b/tests/modules/csm/test_state.py @@ -149,44 +149,55 @@ def test_find_frame_raises_error_for_out_of_range_epoch(): def test_increment_duty_adds_duty_correctly(): state = State() + state._epochs_to_process = tuple(sequence(0, 31)) + state._epochs_per_frame = 32 frame = (0, 31) + duty_epoch, _ = frame state.data = { frame: defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}), } - state.increment_duty(frame, ValidatorIndex(1), True) + state.increment_duty(duty_epoch, ValidatorIndex(1), True) assert state.data[frame][ValidatorIndex(1)].assigned == 11 assert state.data[frame][ValidatorIndex(1)].included == 6 def test_increment_duty_creates_new_validator_entry(): state = State() + state._epochs_to_process = tuple(sequence(0, 31)) + state._epochs_per_frame = 32 frame = (0, 31) + duty_epoch, _ = frame state.data = { frame: defaultdict(AttestationsAccumulator), } - state.increment_duty(frame, ValidatorIndex(2), True) + state.increment_duty(duty_epoch, ValidatorIndex(2), True) assert state.data[frame][ValidatorIndex(2)].assigned == 1 assert state.data[frame][ValidatorIndex(2)].included == 1 def test_increment_duty_handles_non_included_duty(): state = State() + state._epochs_to_process = tuple(sequence(0, 31)) + state._epochs_per_frame = 32 frame = (0, 31) + duty_epoch, _ = frame state.data = { frame: defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}), } - state.increment_duty(frame, ValidatorIndex(1), False) + state.increment_duty(duty_epoch, ValidatorIndex(1), False) assert state.data[frame][ValidatorIndex(1)].assigned == 11 assert state.data[frame][ValidatorIndex(1)].included == 5 def test_increment_duty_raises_error_for_out_of_range_epoch(): state = State() + state._epochs_to_process = tuple(sequence(0, 31)) + state._epochs_per_frame = 32 state.data = { (0, 31): defaultdict(AttestationsAccumulator), } - with pytest.raises(ValueError, match="is not found in the state"): - state.increment_duty((0, 32), ValidatorIndex(1), True) + with pytest.raises(ValueError, match="is out of frames range"): + state.increment_duty(32, ValidatorIndex(1), True) def test_add_processed_epoch_adds_epoch_to_processed_set(): From f9d4f6d711da3db2a5d30084e70ef372621a1bcc Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Wed, 19 Feb 2025 11:34:07 +0100 Subject: [PATCH 018/162] refactor: `init_or_migrate` and `_migrate_frames_data` --- src/modules/csm/state.py | 50 +++++++++++++-------------------- tests/modules/csm/test_state.py | 39 ++++++++++++------------- 2 files changed, 40 insertions(+), 49 deletions(-) diff --git a/src/modules/csm/state.py b/src/modules/csm/state.py index 6ea3db1a2..653193992 100644 --- a/src/modules/csm/state.py +++ b/src/modules/csm/state.py @@ -158,54 +158,44 @@ def init_or_migrate( self.clear() frames = self._calculate_frames(tuple(sequence(l_epoch, r_epoch)), epochs_per_frame) - frames_data: StateData = {frame: defaultdict(AttestationsAccumulator) for frame in frames} if not self.is_empty: cached_frames = self.frames if cached_frames == frames: logger.info({"msg": "No need to migrate duties data cache"}) return + self._migrate_frames_data(frames) + else: + self.data = {frame: defaultdict(AttestationsAccumulator) for frame in frames} - frames_data, migration_status = self._migrate_frames_data(cached_frames, frames) - - for current_frame, migrated in migration_status.items(): - if not migrated: - logger.warning({"msg": f"Invalidating frame duties data cache: {current_frame}"}) - self._processed_epochs.difference_update(sequence(*current_frame)) - - self.data = frames_data self._epochs_per_frame = epochs_per_frame self._epochs_to_process = tuple(sequence(l_epoch, r_epoch)) self._consensus_version = consensus_version self.find_frame.cache_clear() self.commit() - def _migrate_frames_data( - self, current_frames: list[Frame], new_frames: list[Frame] - ) -> tuple[StateData, dict[Frame, bool]]: - migration_status = {frame: False for frame in current_frames} + def _migrate_frames_data(self, new_frames: list[Frame]): + logger.info({"msg": f"Migrating duties data cache: {self.frames=} -> {new_frames=}"}) new_data: StateData = {frame: defaultdict(AttestationsAccumulator) for frame in new_frames} - logger.info({"msg": f"Migrating duties data cache: {current_frames=} -> {new_frames=}"}) + def overlaps(a: Frame, b: Frame): + return a[0] <= b[0] and a[1] >= b[1] - for current_frame in current_frames: - curr_frame_l_epoch, curr_frame_r_epoch = current_frame - for new_frame in new_frames: - if current_frame == new_frame: - new_data[new_frame] = self.data[current_frame] - migration_status[current_frame] = True - break - - new_frame_l_epoch, new_frame_r_epoch = new_frame - if curr_frame_l_epoch >= new_frame_l_epoch and curr_frame_r_epoch <= new_frame_r_epoch: - logger.info({"msg": f"Migrating frame duties data cache: {current_frame=} -> {new_frame=}"}) - for val, duty in self.data[current_frame].items(): + consumed = [] + for new_frame in new_frames: + for frame_to_consume in self.frames: + if overlaps(new_frame, frame_to_consume): + assert frame_to_consume not in consumed + consumed.append(frame_to_consume) + for val, duty in self.data[frame_to_consume].items(): new_data[new_frame][val].assigned += duty.assigned new_data[new_frame][val].included += duty.included - migration_status[current_frame] = True - break - - return new_data, migration_status + for frame in self.frames: + if frame in consumed: + continue + logger.warning({"msg": f"Invalidating frame duties data cache: {frame}"}) + self._processed_epochs -= set(sequence(*frame)) + self.data = new_data def validate(self, l_epoch: EpochNumber, r_epoch: EpochNumber) -> None: if not self.is_fulfilled: diff --git a/tests/modules/csm/test_state.py b/tests/modules/csm/test_state.py index 97004053c..11808b1ea 100644 --- a/tests/modules/csm/test_state.py +++ b/tests/modules/csm/test_state.py @@ -294,72 +294,73 @@ def test_init_or_migrate_discards_unmigrated_frame(): def test_migrate_frames_data_creates_new_data_correctly(): state = State() - current_frames = [(0, 31), (32, 63)] + state._epochs_to_process = tuple(sequence(0, 63)) + state._epochs_per_frame = 32 new_frames = [(0, 63)] state.data = { (0, 31): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}), (32, 63): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(20, 15)}), } - new_data, migration_status = state._migrate_frames_data(current_frames, new_frames) - assert new_data == { + state._migrate_frames_data(new_frames) + assert state.data == { (0, 63): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(30, 20)}) } - assert migration_status == {(0, 31): True, (32, 63): True} def test_migrate_frames_data_handles_no_migration(): state = State() - current_frames = [(0, 31)] + state._epochs_to_process = tuple(sequence(0, 31)) + state._epochs_per_frame = 32 new_frames = [(0, 31)] state.data = { (0, 31): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}), } - new_data, migration_status = state._migrate_frames_data(current_frames, new_frames) - assert new_data == { + state._migrate_frames_data(new_frames) + assert state.data == { (0, 31): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}) } - assert migration_status == {(0, 31): True} def test_migrate_frames_data_handles_partial_migration(): state = State() - current_frames = [(0, 31), (32, 63)] + state._epochs_to_process = tuple(sequence(0, 63)) + state._epochs_per_frame = 32 new_frames = [(0, 31), (32, 95)] state.data = { (0, 31): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}), (32, 63): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(20, 15)}), } - new_data, migration_status = state._migrate_frames_data(current_frames, new_frames) - assert new_data == { + state._migrate_frames_data(new_frames) + assert state.data == { (0, 31): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}), (32, 95): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(20, 15)}), } - assert migration_status == {(0, 31): True, (32, 63): True} def test_migrate_frames_data_handles_no_data(): state = State() + state._epochs_to_process = tuple(sequence(0, 31)) + state._epochs_per_frame = 32 current_frames = [(0, 31)] new_frames = [(0, 31)] state.data = {frame: defaultdict(AttestationsAccumulator) for frame in current_frames} - new_data, migration_status = state._migrate_frames_data(current_frames, new_frames) - assert new_data == {(0, 31): defaultdict(AttestationsAccumulator)} - assert migration_status == {(0, 31): True} + state._migrate_frames_data(new_frames) + assert state.data == {(0, 31): defaultdict(AttestationsAccumulator)} def test_migrate_frames_data_handles_wider_old_frame(): state = State() - current_frames = [(0, 63)] + state._epochs_to_process = tuple(sequence(0, 63)) + state._epochs_per_frame = 64 new_frames = [(0, 31), (32, 63)] state.data = { (0, 63): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(30, 20)}), } - new_data, migration_status = state._migrate_frames_data(current_frames, new_frames) - assert new_data == { + state._migrate_frames_data(new_frames) + assert state.data == { (0, 31): defaultdict(AttestationsAccumulator), (32, 63): defaultdict(AttestationsAccumulator), } - assert migration_status == {(0, 63): False} def test_validate_raises_error_if_state_not_fulfilled(): From 84eb94f877656a71a414ee7479fe8bc500a84673 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Wed, 19 Feb 2025 11:35:07 +0100 Subject: [PATCH 019/162] refactor: `find_frame` --- src/modules/csm/state.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/modules/csm/state.py b/src/modules/csm/state.py index 653193992..cb33c499c 100644 --- a/src/modules/csm/state.py +++ b/src/modules/csm/state.py @@ -131,7 +131,8 @@ def clear(self) -> None: @lru_cache(variables.CSM_ORACLE_MAX_CONCURRENCY) def find_frame(self, epoch: EpochNumber) -> Frame: for epoch_range in self.frames: - if epoch_range[0] <= epoch <= epoch_range[1]: + from_epoch, to_epoch = epoch_range + if from_epoch <= epoch <= to_epoch: return epoch_range raise ValueError(f"Epoch {epoch} is out of frames range: {self.frames}") From ac0d8b0b6ff9337bec25ec4d606e4f62ac9a0c89 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Wed, 19 Feb 2025 11:35:38 +0100 Subject: [PATCH 020/162] refactor: `init_or_migrate` -> `migrate` --- src/modules/csm/csm.py | 2 +- src/modules/csm/state.py | 2 +- tests/modules/csm/test_checkpoint.py | 8 ++++---- tests/modules/csm/test_state.py | 10 +++++----- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index 4781e6f00..5fe6f7a94 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -200,7 +200,7 @@ def collect_data(self, blockstamp: BlockStamp) -> bool: logger.info({"msg": "The starting epoch of the epochs range is not finalized yet"}) return False - self.state.init_or_migrate(l_epoch, r_epoch, converter.frame_config.epochs_per_frame, consensus_version) + self.state.migrate(l_epoch, r_epoch, converter.frame_config.epochs_per_frame, consensus_version) self.state.log_progress() if self.state.is_fulfilled: diff --git a/src/modules/csm/state.py b/src/modules/csm/state.py index cb33c499c..2b9abeb86 100644 --- a/src/modules/csm/state.py +++ b/src/modules/csm/state.py @@ -146,7 +146,7 @@ def add_processed_epoch(self, epoch: EpochNumber) -> None: def log_progress(self) -> None: logger.info({"msg": f"Processed {len(self._processed_epochs)} of {len(self._epochs_to_process)} epochs"}) - def init_or_migrate( + def migrate( self, l_epoch: EpochNumber, r_epoch: EpochNumber, epochs_per_frame: int, consensus_version: int ) -> None: if consensus_version != self._consensus_version: diff --git a/tests/modules/csm/test_checkpoint.py b/tests/modules/csm/test_checkpoint.py index 4b456ed03..070d82bf9 100644 --- a/tests/modules/csm/test_checkpoint.py +++ b/tests/modules/csm/test_checkpoint.py @@ -326,7 +326,7 @@ def test_checkpoints_processor_no_eip7549_support( monkeypatch: pytest.MonkeyPatch, ): state = State() - state.init_or_migrate(EpochNumber(0), EpochNumber(255), 256, 1) + state.migrate(EpochNumber(0), EpochNumber(255), 256, 1) processor = FrameCheckpointProcessor( consensus_client, state, @@ -354,7 +354,7 @@ def test_checkpoints_processor_check_duty( converter, ): state = State() - state.init_or_migrate(0, 255, 256, 1) + state.migrate(0, 255, 256, 1) finalized_blockstamp = ... processor = FrameCheckpointProcessor( consensus_client, @@ -379,7 +379,7 @@ def test_checkpoints_processor_process( converter, ): state = State() - state.init_or_migrate(0, 255, 256, 1) + state.migrate(0, 255, 256, 1) finalized_blockstamp = ... processor = FrameCheckpointProcessor( consensus_client, @@ -404,7 +404,7 @@ def test_checkpoints_processor_exec( converter, ): state = State() - state.init_or_migrate(0, 255, 256, 1) + state.migrate(0, 255, 256, 1) finalized_blockstamp = ... processor = FrameCheckpointProcessor( consensus_client, diff --git a/tests/modules/csm/test_state.py b/tests/modules/csm/test_state.py index 11808b1ea..4eba5cfd0 100644 --- a/tests/modules/csm/test_state.py +++ b/tests/modules/csm/test_state.py @@ -218,7 +218,7 @@ def test_init_or_migrate_discards_data_on_version_change(): state._consensus_version = 1 state.clear = Mock() state.commit = Mock() - state.init_or_migrate(0, 63, 32, 2) + state.migrate(0, 63, 32, 2) state.clear.assert_called_once() state.commit.assert_called_once() @@ -233,7 +233,7 @@ def test_init_or_migrate_no_migration_needed(): (32, 63): defaultdict(AttestationsAccumulator), } state.commit = Mock() - state.init_or_migrate(0, 63, 32, 1) + state.migrate(0, 63, 32, 1) state.commit.assert_not_called() @@ -247,7 +247,7 @@ def test_init_or_migrate_migrates_data(): (32, 63): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(20, 15)}), } state.commit = Mock() - state.init_or_migrate(0, 63, 64, 1) + state.migrate(0, 63, 64, 1) assert state.data == { (0, 63): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(30, 20)}), } @@ -263,7 +263,7 @@ def test_init_or_migrate_invalidates_unmigrated_frames(): (0, 63): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(30, 20)}), } state.commit = Mock() - state.init_or_migrate(0, 31, 32, 1) + state.migrate(0, 31, 32, 1) assert state.data == { (0, 31): defaultdict(AttestationsAccumulator), } @@ -283,7 +283,7 @@ def test_init_or_migrate_discards_unmigrated_frame(): } state._processed_epochs = set(sequence(0, 95)) state.commit = Mock() - state.init_or_migrate(0, 63, 32, 1) + state.migrate(0, 63, 32, 1) assert state.data == { (0, 31): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}), (32, 63): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(20, 15)}), From 9583a6b488a571cd1b985f4e390ce18e6ccd88d1 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Wed, 19 Feb 2025 11:43:22 +0100 Subject: [PATCH 021/162] refactor: `calculate_distribution` types --- src/modules/csm/csm.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index 5fe6f7a94..39f22796d 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -3,6 +3,7 @@ from typing import Iterator from hexbytes import HexBytes +from web3.types import Wei from src.constants import TOTAL_BASIS_POINTS, UINT64_MAX from src.metrics.prometheus.business import CONTRACT_ON_PAUSE @@ -226,12 +227,12 @@ def collect_data(self, blockstamp: BlockStamp) -> bool: def calculate_distribution( self, blockstamp: ReferenceBlockStamp - ) -> tuple[int, defaultdict[NodeOperatorId, int], list[FramePerfLog]]: + ) -> tuple[Shares, defaultdict[NodeOperatorId, Shares], list[FramePerfLog]]: """Computes distribution of fee shares at the given timestamp""" operators_to_validators = self.module_validators_by_node_operators(blockstamp) - total_distributed = 0 - total_rewards = defaultdict[NodeOperatorId, int](int) + total_distributed = Shares(0) + total_rewards = defaultdict[NodeOperatorId, Shares](Shares) logs: list[FramePerfLog] = [] for frame in self.state.frames: From 80dc2f6facf8f6eb0999dcfca6c665cbf58ec93d Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Wed, 19 Feb 2025 11:45:25 +0100 Subject: [PATCH 022/162] refactor: `test_csm_module.py` --- tests/modules/csm/test_csm_module.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/tests/modules/csm/test_csm_module.py b/tests/modules/csm/test_csm_module.py index 8fd863cd5..ee096b64d 100644 --- a/tests/modules/csm/test_csm_module.py +++ b/tests/modules/csm/test_csm_module.py @@ -21,7 +21,7 @@ @pytest.fixture(autouse=True) -def mock_get_module_id(monkeypatch: pytest.MonkeyPatch): +def mock_get_staking_module(monkeypatch: pytest.MonkeyPatch): monkeypatch.setattr(CSOracle, "_get_staking_module", Mock()) @@ -41,19 +41,18 @@ def test_init(module: CSOracle): def test_get_stuck_operators(module: CSOracle, csm: CSM): module.module = Mock() # type: ignore - module.module_id = StakingModuleId(1) module.w3.cc = Mock() module.w3.lido_validators = Mock() module.w3.lido_contracts = Mock() module.w3.lido_contracts.staking_router.get_all_node_operator_digests = Mock( return_value=[ - type('NodeOperator', (object,), {'id': 0, 'stuck_validators_count': 0})(), - type('NodeOperator', (object,), {'id': 1, 'stuck_validators_count': 0})(), - type('NodeOperator', (object,), {'id': 2, 'stuck_validators_count': 1})(), - type('NodeOperator', (object,), {'id': 3, 'stuck_validators_count': 0})(), - type('NodeOperator', (object,), {'id': 4, 'stuck_validators_count': 100500})(), - type('NodeOperator', (object,), {'id': 5, 'stuck_validators_count': 100})(), - type('NodeOperator', (object,), {'id': 6, 'stuck_validators_count': 0})(), + Mock(id=0, stuck_validators_count=0), + Mock(id=1, stuck_validators_count=0), + Mock(id=2, stuck_validators_count=1), + Mock(id=3, stuck_validators_count=0), + Mock(id=4, stuck_validators_count=100500), + Mock(id=5, stuck_validators_count=100), + Mock(id=6, stuck_validators_count=0), ] ) @@ -80,7 +79,6 @@ def test_get_stuck_operators(module: CSOracle, csm: CSM): def test_get_stuck_operators_left_border_before_enact(module: CSOracle, csm: CSM, caplog: pytest.LogCaptureFixture): module.module = Mock() # type: ignore - module.module_id = StakingModuleId(3) module.w3.cc = Mock() module.w3.lido_validators = Mock() module.w3.lido_contracts = Mock() From 0d122e87786d5d0c0523de71938673bacb26a229 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Wed, 19 Feb 2025 11:52:39 +0100 Subject: [PATCH 023/162] feat: add negative value checking in `calc_rewards_distribution_in_frame` --- src/modules/csm/csm.py | 2 ++ tests/modules/csm/test_csm_distribution.py | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index 39f22796d..be5dd6f2a 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -344,6 +344,8 @@ def calc_rewards_distribution_in_frame( participation_shares: dict[NodeOperatorId, int], rewards_to_distribute: int, ) -> dict[NodeOperatorId, int]: + if rewards_to_distribute < 0: + raise ValueError(f"Invalid rewards to distribute: {rewards_to_distribute}") rewards_distribution: dict[NodeOperatorId, int] = defaultdict(int) total_participation = sum(participation_shares.values()) diff --git a/tests/modules/csm/test_csm_distribution.py b/tests/modules/csm/test_csm_distribution.py index c2c04591a..f277f6eb8 100644 --- a/tests/modules/csm/test_csm_distribution.py +++ b/tests/modules/csm/test_csm_distribution.py @@ -327,3 +327,11 @@ def test_calc_rewards_distribution_in_frame_handles_partial_participation(): assert rewards_distribution[NodeOperatorId(1)] == Wei(1 * 10**18) assert rewards_distribution[NodeOperatorId(2)] == 0 + + +def test_calc_rewards_distribution_in_frame_handles_negative_to_distribute(): + participation_shares = {NodeOperatorId(1): 100, NodeOperatorId(2): 200} + rewards_to_distribute = Wei(-1) + + with pytest.raises(ValueError, match="Invalid rewards to distribute"): + CSOracle.calc_rewards_distribution_in_frame(participation_shares, rewards_to_distribute) From f9290da6ecc3711e2867d897366a25f33fe41c89 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Wed, 19 Feb 2025 11:53:40 +0100 Subject: [PATCH 024/162] fix: linter --- src/modules/csm/csm.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index be5dd6f2a..e9e042172 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -3,7 +3,6 @@ from typing import Iterator from hexbytes import HexBytes -from web3.types import Wei from src.constants import TOTAL_BASIS_POINTS, UINT64_MAX from src.metrics.prometheus.business import CONTRACT_ON_PAUSE From 0731525c1f27cd2578b1822fee65311e6798f026 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Wed, 19 Feb 2025 12:39:44 +0100 Subject: [PATCH 025/162] refactor: `frames` now is `State` attribute --- src/modules/csm/state.py | 9 ++---- tests/modules/csm/test_state.py | 52 ++++++++++++--------------------- 2 files changed, 21 insertions(+), 40 deletions(-) diff --git a/src/modules/csm/state.py b/src/modules/csm/state.py index 2b9abeb86..2f284f980 100644 --- a/src/modules/csm/state.py +++ b/src/modules/csm/state.py @@ -49,11 +49,11 @@ class State: The state can be migrated to be used for another frame's report by calling the `migrate` method. """ + frames: list[Frame] data: StateData _epochs_to_process: tuple[EpochNumber, ...] _processed_epochs: set[EpochNumber] - _epochs_per_frame: int _consensus_version: int = 1 @@ -61,7 +61,6 @@ def __init__(self) -> None: self.data = {} self._epochs_to_process = tuple() self._processed_epochs = set() - self._epochs_per_frame = 0 EXTENSION = ".pkl" @@ -111,10 +110,6 @@ def unprocessed_epochs(self) -> set[EpochNumber]: def is_fulfilled(self) -> bool: return not self.unprocessed_epochs - @property - def frames(self): - return self._calculate_frames(self._epochs_to_process, self._epochs_per_frame) - @staticmethod def _calculate_frames(epochs_to_process: tuple[EpochNumber, ...], epochs_per_frame: int) -> list[Frame]: """Split epochs to process into frames of `epochs_per_frame` length""" @@ -169,7 +164,7 @@ def migrate( else: self.data = {frame: defaultdict(AttestationsAccumulator) for frame in frames} - self._epochs_per_frame = epochs_per_frame + self.frames = frames self._epochs_to_process = tuple(sequence(l_epoch, r_epoch)) self._consensus_version = consensus_version self.find_frame.cache_clear() diff --git a/tests/modules/csm/test_state.py b/tests/modules/csm/test_state.py index 4eba5cfd0..2fb3cd078 100644 --- a/tests/modules/csm/test_state.py +++ b/tests/modules/csm/test_state.py @@ -132,16 +132,14 @@ def test_clear_resets_state_to_empty(): def test_find_frame_returns_correct_frame(): state = State() - state._epochs_to_process = tuple(sequence(0, 31)) - state._epochs_per_frame = 32 + state.frames = [(0, 31)] state.data = {(0, 31): defaultdict(AttestationsAccumulator)} assert state.find_frame(15) == (0, 31) def test_find_frame_raises_error_for_out_of_range_epoch(): state = State() - state._epochs_to_process = tuple(sequence(0, 31)) - state._epochs_per_frame = 32 + state.frames = [(0, 31)] state.data = {(0, 31): defaultdict(AttestationsAccumulator)} with pytest.raises(ValueError, match="Epoch 32 is out of frames range"): state.find_frame(32) @@ -149,9 +147,8 @@ def test_find_frame_raises_error_for_out_of_range_epoch(): def test_increment_duty_adds_duty_correctly(): state = State() - state._epochs_to_process = tuple(sequence(0, 31)) - state._epochs_per_frame = 32 frame = (0, 31) + state.frames = [frame] duty_epoch, _ = frame state.data = { frame: defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}), @@ -163,9 +160,8 @@ def test_increment_duty_adds_duty_correctly(): def test_increment_duty_creates_new_validator_entry(): state = State() - state._epochs_to_process = tuple(sequence(0, 31)) - state._epochs_per_frame = 32 frame = (0, 31) + state.frames = [frame] duty_epoch, _ = frame state.data = { frame: defaultdict(AttestationsAccumulator), @@ -177,8 +173,8 @@ def test_increment_duty_creates_new_validator_entry(): def test_increment_duty_handles_non_included_duty(): state = State() - state._epochs_to_process = tuple(sequence(0, 31)) - state._epochs_per_frame = 32 + frame = (0, 31) + state.frames = [frame] frame = (0, 31) duty_epoch, _ = frame state.data = { @@ -191,10 +187,10 @@ def test_increment_duty_handles_non_included_duty(): def test_increment_duty_raises_error_for_out_of_range_epoch(): state = State() - state._epochs_to_process = tuple(sequence(0, 31)) - state._epochs_per_frame = 32 + frame = (0, 31) + state.frames = [frame] state.data = { - (0, 31): defaultdict(AttestationsAccumulator), + frame: defaultdict(AttestationsAccumulator), } with pytest.raises(ValueError, match="is out of frames range"): state.increment_duty(32, ValidatorIndex(1), True) @@ -226,8 +222,7 @@ def test_init_or_migrate_discards_data_on_version_change(): def test_init_or_migrate_no_migration_needed(): state = State() state._consensus_version = 1 - state._epochs_to_process = tuple(sequence(0, 63)) - state._epochs_per_frame = 32 + state.frames = [(0, 31), (32, 63)] state.data = { (0, 31): defaultdict(AttestationsAccumulator), (32, 63): defaultdict(AttestationsAccumulator), @@ -240,8 +235,7 @@ def test_init_or_migrate_no_migration_needed(): def test_init_or_migrate_migrates_data(): state = State() state._consensus_version = 1 - state._epochs_to_process = tuple(sequence(0, 63)) - state._epochs_per_frame = 32 + state.frames = [(0, 31), (32, 63)] state.data = { (0, 31): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}), (32, 63): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(20, 15)}), @@ -257,8 +251,7 @@ def test_init_or_migrate_migrates_data(): def test_init_or_migrate_invalidates_unmigrated_frames(): state = State() state._consensus_version = 1 - state._epochs_to_process = tuple(sequence(0, 63)) - state._epochs_per_frame = 64 + state.frames = [(0, 63)] state.data = { (0, 63): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(30, 20)}), } @@ -274,8 +267,7 @@ def test_init_or_migrate_invalidates_unmigrated_frames(): def test_init_or_migrate_discards_unmigrated_frame(): state = State() state._consensus_version = 1 - state._epochs_to_process = tuple(sequence(0, 95)) - state._epochs_per_frame = 32 + state.frames = [(0, 31), (32, 63), (64, 95)] state.data = { (0, 31): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}), (32, 63): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(20, 15)}), @@ -294,8 +286,7 @@ def test_init_or_migrate_discards_unmigrated_frame(): def test_migrate_frames_data_creates_new_data_correctly(): state = State() - state._epochs_to_process = tuple(sequence(0, 63)) - state._epochs_per_frame = 32 + state.frames = [(0, 31), (32, 63)] new_frames = [(0, 63)] state.data = { (0, 31): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}), @@ -309,8 +300,7 @@ def test_migrate_frames_data_creates_new_data_correctly(): def test_migrate_frames_data_handles_no_migration(): state = State() - state._epochs_to_process = tuple(sequence(0, 31)) - state._epochs_per_frame = 32 + state.frames = [(0, 31)] new_frames = [(0, 31)] state.data = { (0, 31): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}), @@ -323,8 +313,7 @@ def test_migrate_frames_data_handles_no_migration(): def test_migrate_frames_data_handles_partial_migration(): state = State() - state._epochs_to_process = tuple(sequence(0, 63)) - state._epochs_per_frame = 32 + state.frames = [(0, 31), (32, 63)] new_frames = [(0, 31), (32, 95)] state.data = { (0, 31): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(10, 5)}), @@ -339,19 +328,16 @@ def test_migrate_frames_data_handles_partial_migration(): def test_migrate_frames_data_handles_no_data(): state = State() - state._epochs_to_process = tuple(sequence(0, 31)) - state._epochs_per_frame = 32 - current_frames = [(0, 31)] + state.frames = [(0, 31)] new_frames = [(0, 31)] - state.data = {frame: defaultdict(AttestationsAccumulator) for frame in current_frames} + state.data = {frame: defaultdict(AttestationsAccumulator) for frame in state.frames} state._migrate_frames_data(new_frames) assert state.data == {(0, 31): defaultdict(AttestationsAccumulator)} def test_migrate_frames_data_handles_wider_old_frame(): state = State() - state._epochs_to_process = tuple(sequence(0, 63)) - state._epochs_per_frame = 64 + state.frames = [(0, 63)] new_frames = [(0, 31), (32, 63)] state.data = { (0, 63): defaultdict(AttestationsAccumulator, {ValidatorIndex(1): AttestationsAccumulator(30, 20)}), From 15d5ab2681e7f44f18b0278bd8c51cd5357a0e12 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Wed, 19 Feb 2025 12:53:46 +0100 Subject: [PATCH 026/162] refactor: `migrate` --- src/modules/csm/state.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/modules/csm/state.py b/src/modules/csm/state.py index 2f284f980..48debfe83 100644 --- a/src/modules/csm/state.py +++ b/src/modules/csm/state.py @@ -58,6 +58,7 @@ class State: _consensus_version: int = 1 def __init__(self) -> None: + self.frames = [] self.data = {} self._epochs_to_process = tuple() self._processed_epochs = set() @@ -153,21 +154,16 @@ def migrate( ) self.clear() - frames = self._calculate_frames(tuple(sequence(l_epoch, r_epoch)), epochs_per_frame) + new_frames = self._calculate_frames(tuple(sequence(l_epoch, r_epoch)), epochs_per_frame) + if self.frames == new_frames: + logger.info({"msg": "No need to migrate duties data cache"}) + return + self._migrate_frames_data(new_frames) - if not self.is_empty: - cached_frames = self.frames - if cached_frames == frames: - logger.info({"msg": "No need to migrate duties data cache"}) - return - self._migrate_frames_data(frames) - else: - self.data = {frame: defaultdict(AttestationsAccumulator) for frame in frames} - - self.frames = frames + self.frames = new_frames + self.find_frame.cache_clear() self._epochs_to_process = tuple(sequence(l_epoch, r_epoch)) self._consensus_version = consensus_version - self.find_frame.cache_clear() self.commit() def _migrate_frames_data(self, new_frames: list[Frame]): From 703af0226b2f857c0ca6ac702f7f584c0737d672 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Wed, 19 Feb 2025 17:20:51 +0100 Subject: [PATCH 027/162] chore: sync with base --- src/modules/csm/csm.py | 136 +------------------------------- src/modules/csm/distribution.py | 3 +- 2 files changed, 3 insertions(+), 136 deletions(-) diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index 460007a05..71ace141a 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -29,7 +29,6 @@ SlotNumber, ) from src.utils.cache import global_lru_cache as lru_cache -from src.utils.slot import get_reference_blockstamp from src.utils.web3converter import Web3Converter from src.web3py.extensions.lido_validators import NodeOperatorId, StakingModule from src.web3py.types import Web3 @@ -55,8 +54,7 @@ class CSOracle(BaseModule, ConsensusModule): 2. Calculate the performance of each validator based on the attestations. 3. Calculate the share of each CSM node operator excluding underperforming validators. """ - # TODO: should be (Contract=2, Consensus=3) - COMPATIBLE_ONCHAIN_VERSIONS = [(2, 2)] + COMPATIBLE_ONCHAIN_VERSIONS = [(2, 3)] report_contract: CSFeeOracleContract staking_module: StakingModule @@ -223,138 +221,6 @@ def collect_data(self, blockstamp: BlockStamp) -> bool: return self.state.is_fulfilled - def calculate_distribution( - self, blockstamp: ReferenceBlockStamp - ) -> tuple[Shares, defaultdict[NodeOperatorId, Shares], list[FramePerfLog]]: - """Computes distribution of fee shares at the given timestamp""" - operators_to_validators = self.module_validators_by_node_operators(blockstamp) - - total_distributed = Shares(0) - total_rewards = defaultdict[NodeOperatorId, Shares](Shares) - logs: list[FramePerfLog] = [] - - for frame in self.state.frames: - from_epoch, to_epoch = frame - logger.info({"msg": f"Calculating distribution for frame [{from_epoch};{to_epoch}]"}) - - frame_blockstamp = blockstamp - if to_epoch != blockstamp.ref_epoch: - frame_blockstamp = self._get_ref_blockstamp_for_frame(blockstamp, to_epoch) - - total_rewards_to_distribute = self.w3.csm.fee_distributor.shares_to_distribute(frame_blockstamp.block_hash) - rewards_to_distribute_in_frame = total_rewards_to_distribute - total_distributed - - rewards_in_frame, log = self._calculate_distribution_in_frame( - frame, frame_blockstamp, rewards_to_distribute_in_frame, operators_to_validators - ) - distributed_in_frame = sum(rewards_in_frame.values()) - - total_distributed += distributed_in_frame - if total_distributed > total_rewards_to_distribute: - raise CSMError(f"Invalid distribution: {total_distributed=} > {total_rewards_to_distribute=}") - - for no_id, rewards in rewards_in_frame.items(): - total_rewards[no_id] += rewards - - logs.append(log) - - return total_distributed, total_rewards, logs - - def _get_ref_blockstamp_for_frame( - self, blockstamp: ReferenceBlockStamp, frame_ref_epoch: EpochNumber - ) -> ReferenceBlockStamp: - converter = self.converter(blockstamp) - return get_reference_blockstamp( - cc=self.w3.cc, - ref_slot=converter.get_epoch_last_slot(frame_ref_epoch), - ref_epoch=frame_ref_epoch, - last_finalized_slot_number=blockstamp.slot_number, - ) - - def _calculate_distribution_in_frame( - self, - frame: Frame, - blockstamp: ReferenceBlockStamp, - rewards_to_distribute: int, - operators_to_validators: ValidatorsByNodeOperator - ): - threshold = self._get_performance_threshold(frame, blockstamp) - log = FramePerfLog(blockstamp, frame, threshold) - - participation_shares: defaultdict[NodeOperatorId, int] = defaultdict(int) - - stuck_operators = self.get_stuck_operators(frame, blockstamp) - for (_, no_id), validators in operators_to_validators.items(): - log_operator = log.operators[no_id] - if no_id in stuck_operators: - log_operator.stuck = True - continue - for validator in validators: - duty = self.state.data[frame].get(validator.index) - self.process_validator_duty(validator, duty, threshold, participation_shares, log_operator) - - rewards_distribution = self.calc_rewards_distribution_in_frame(participation_shares, rewards_to_distribute) - - for no_id, no_rewards in rewards_distribution.items(): - log.operators[no_id].distributed = no_rewards - - log.distributable = rewards_to_distribute - - return rewards_distribution, log - - def _get_performance_threshold(self, frame: Frame, blockstamp: ReferenceBlockStamp) -> float: - network_perf = self.state.get_network_aggr(frame).perf - perf_leeway = self.w3.csm.oracle.perf_leeway_bp(blockstamp.block_hash) / TOTAL_BASIS_POINTS - threshold = network_perf - perf_leeway - return threshold - - @staticmethod - def process_validator_duty( - validator: LidoValidator, - attestation_duty: AttestationsAccumulator | None, - threshold: float, - participation_shares: defaultdict[NodeOperatorId, int], - log_operator: OperatorFrameSummary - ): - if attestation_duty is None: - # It's possible that the validator is not assigned to any duty, hence it's performance - # is not presented in the aggregates (e.g. exited, pending for activation etc). - # TODO: check `sync_aggr` to strike (in case of bad sync performance) after validator exit - return - - log_validator = log_operator.validators[validator.index] - - if validator.validator.slashed is True: - # It means that validator was active during the frame and got slashed and didn't meet the exit - # epoch, so we should not count such validator for operator's share. - log_validator.slashed = True - return - - if attestation_duty.perf > threshold: - # Count of assigned attestations used as a metrics of time - # the validator was active in the current frame. - participation_shares[validator.lido_id.operatorIndex] += attestation_duty.assigned - - log_validator.attestation_duty = attestation_duty - - @staticmethod - def calc_rewards_distribution_in_frame( - participation_shares: dict[NodeOperatorId, int], - rewards_to_distribute: int, - ) -> dict[NodeOperatorId, int]: - if rewards_to_distribute < 0: - raise ValueError(f"Invalid rewards to distribute: {rewards_to_distribute}") - rewards_distribution: dict[NodeOperatorId, int] = defaultdict(int) - total_participation = sum(participation_shares.values()) - - for no_id, no_participation_share in participation_shares.items(): - if no_participation_share == 0: - # Skip operators with zero participation - continue - rewards_distribution[no_id] = rewards_to_distribute * no_participation_share // total_participation - - return rewards_distribution - def get_accumulated_rewards(self, cid: CID, root: HexBytes) -> Iterator[tuple[NodeOperatorId, Shares]]: logger.info({"msg": "Fetching tree by CID from IPFS", "cid": repr(cid)}) tree = Tree.decode(self.w3.ipfs.fetch(cid)) diff --git a/src/modules/csm/distribution.py b/src/modules/csm/distribution.py index 8c961fbdf..1c5eed162 100644 --- a/src/modules/csm/distribution.py +++ b/src/modules/csm/distribution.py @@ -5,6 +5,7 @@ from src.constants import UINT256_MAX, TOTAL_BASIS_POINTS from src.modules.csm.log import FramePerfLog, OperatorFrameSummary from src.modules.csm.state import DutyAccumulator, Frame, State +from src.modules.csm.types import Shares from src.providers.execution.contracts.cs_parameters_registry import PerformanceCoefficients from src.types import NodeOperatorId, ReferenceBlockStamp, EpochNumber, StakingModuleAddress from src.utils.blockstamp import build_blockstamp @@ -31,7 +32,7 @@ def __init__(self, w3: Web3, staking_module: StakingModule, converter: Web3Conve def calculate( self, blockstamp: ReferenceBlockStamp - ) -> tuple[int, defaultdict[NodeOperatorId, int], int, list[FramePerfLog]]: + ) -> tuple[Shares, defaultdict[NodeOperatorId, Shares], Shares, list[FramePerfLog]]: """Computes distribution of fee shares at the given timestamp""" total_distributed_rewards = 0 total_rewards_map = defaultdict[NodeOperatorId, int](int) From 6a9a89898e2f01dcdeaa47ed17423bb198dc24d9 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Thu, 20 Feb 2025 17:30:48 +0100 Subject: [PATCH 028/162] wip --- src/modules/csm/checkpoint.py | 40 +++--- src/modules/csm/csm.py | 4 +- src/modules/csm/distribution.py | 121 +++++++----------- src/modules/csm/log.py | 2 +- src/modules/csm/state.py | 89 ++++++------- .../contracts/cs_parameters_registry.py | 38 +++++- 6 files changed, 146 insertions(+), 148 deletions(-) diff --git a/src/modules/csm/checkpoint.py b/src/modules/csm/checkpoint.py index 69ec412ec..a26599b80 100644 --- a/src/modules/csm/checkpoint.py +++ b/src/modules/csm/checkpoint.py @@ -122,7 +122,7 @@ def __setitem__(self, committee_network_index: int, value: SyncCommittee | None) super().__setitem__(committee_network_index, value) -SYNC_COMMITTEE_CACHE = SyncCommitteesCache() +SYNC_COMMITTEES_CACHE = SyncCommitteesCache() class FrameCheckpointProcessor: @@ -308,7 +308,8 @@ def _prepare_sync_committee( self, epoch: EpochNumber, duty_block_roots: list[SlotBlockRoot] ) -> dict[SlotNumber, list[ValidatorDuty]]: - sync_committee = self._get_cached_sync_committee(epoch) + with lock: + sync_committee = self._get_sync_committee(epoch) duties = {} for slot, root in duty_block_roots: @@ -322,24 +323,23 @@ def _prepare_sync_committee( return duties - def _get_cached_sync_committee(self, epoch: EpochNumber) -> SyncCommittee: - sync_committee_index = epoch // EPOCHS_PER_SYNC_COMMITTEE_PERIOD - with lock: - sync_committee = SYNC_COMMITTEE_CACHE.get(sync_committee_index) - if not sync_committee: - epochs_before_new_sync_committee = epoch % EPOCHS_PER_SYNC_COMMITTEE_PERIOD - epochs_range = EPOCHS_PER_SYNC_COMMITTEE_PERIOD - epochs_before_new_sync_committee - logger.info({"msg": f"Preparing cached Sync Committee for {epochs_range} epochs from {epoch} epoch"}) - state_blockstamp = build_blockstamp( - get_prev_non_missed_slot( - self.cc, - self.converter.get_epoch_first_slot(epoch), - self.finalized_blockstamp.slot_number - ) - ) - sync_committee = self.cc.get_sync_committee(state_blockstamp, epoch) - SYNC_COMMITTEE_CACHE[sync_committee_index] = sync_committee - return sync_committee + def _get_sync_committee(self, epoch: EpochNumber) -> SyncCommittee: + sync_committee_network_index = epoch // EPOCHS_PER_SYNC_COMMITTEE_PERIOD + if cached_sync_committee := SYNC_COMMITTEES_CACHE.get(sync_committee_network_index): + return cached_sync_committee + from_epoch = EpochNumber(epoch - epoch % EPOCHS_PER_SYNC_COMMITTEE_PERIOD) + to_epoch = EpochNumber(from_epoch + EPOCHS_PER_SYNC_COMMITTEE_PERIOD - 1) + logger.info({"msg": f"Preparing cached Sync Committee for [{from_epoch};{to_epoch}] chain epochs"}) + state_blockstamp = build_blockstamp( + get_prev_non_missed_slot( + self.cc, + self.converter.get_epoch_first_slot(epoch), + self.finalized_blockstamp.slot_number + ) + ) + sync_committee = self.cc.get_sync_committee(state_blockstamp, epoch) + SYNC_COMMITTEES_CACHE[sync_committee_network_index] = sync_committee + return sync_committee @timeit( lambda args, duration: logger.info( diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index 71ace141a..f66d3b04a 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -114,7 +114,7 @@ def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: log_cid=logs_cid, distributed=0, rebate=total_rebate, - strikes_tree_root=ZERO_HASH, + strikes_tree_root=HexBytes(ZERO_HASH), strikes_tree_cid="", ).as_tuple() @@ -136,7 +136,7 @@ def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: log_cid=logs_cid, distributed=total_distributed_rewards, rebate=total_rebate, - strikes_tree_root=ZERO_HASH, + strikes_tree_root=HexBytes(ZERO_HASH), strikes_tree_cid="", ).as_tuple() diff --git a/src/modules/csm/distribution.py b/src/modules/csm/distribution.py index 1c5eed162..7d3919055 100644 --- a/src/modules/csm/distribution.py +++ b/src/modules/csm/distribution.py @@ -2,7 +2,6 @@ from collections import defaultdict from functools import lru_cache -from src.constants import UINT256_MAX, TOTAL_BASIS_POINTS from src.modules.csm.log import FramePerfLog, OperatorFrameSummary from src.modules.csm.state import DutyAccumulator, Frame, State from src.modules.csm.types import Shares @@ -18,7 +17,6 @@ class Distribution: - w3: Web3 staking_module: StakingModule converter: Web3Converter @@ -41,7 +39,7 @@ def calculate( for frame in self.state.frames: logger.info({"msg": f"Calculating distribution for {frame=}"}) - from_epoch, to_epoch = frame + _, to_epoch = frame frame_blockstamp = self._get_frame_blockstamp(blockstamp, to_epoch) frame_module_validators = self._get_module_validators(frame_blockstamp) @@ -89,7 +87,7 @@ def _calculate_distribution_in_frame( frame: Frame, blockstamp: ReferenceBlockStamp, rewards_to_distribute: int, - operators_to_validators: ValidatorsByNodeOperator + operators_to_validators: ValidatorsByNodeOperator, ) -> tuple[dict[NodeOperatorId, int], FramePerfLog]: total_rebate_share = 0 participation_shares: defaultdict[NodeOperatorId, int] = defaultdict(int) @@ -97,6 +95,11 @@ def _calculate_distribution_in_frame( network_perf = self._get_network_performance(frame) + # TODO: get curves count from the contract + # curves_count = self.w3.csm.accounting.get_curves_count(blockstamp.block_hash) + curves_count = 2 + _cached_curve_params = lru_cache(maxsize=curves_count)(self._get_curve_params) + stuck_operators = self._get_stuck_operators(frame, blockstamp) for (_, no_id), validators in operators_to_validators.items(): logger.info({"msg": f"Calculating distribution for {no_id=}"}) @@ -106,56 +109,33 @@ def _calculate_distribution_in_frame( continue curve_id = self.w3.csm.accounting.get_bond_curve_id(no_id, blockstamp.block_hash) - perf_coeffs, perf_leeway_data, reward_share_data = self._get_curve_params(curve_id, blockstamp) + perf_coeffs, perf_leeway, reward_share = _cached_curve_params(curve_id, blockstamp) sorted_validators = sorted(validators, key=lambda v: v.index) - for number, validator in enumerate(sorted_validators): - att_duty = self.state.att_data[frame].get(validator.index) - prop_duty = self.state.prop_data[frame].get(validator.index) - sync_duty = self.state.sync_data[frame].get(validator.index) - - if att_duty is None: - # It's possible that the validator is not assigned to any duty, hence it's performance - # is not presented in the aggregates (e.g. exited, pending for activation etc). - # TODO: check `sync_aggr` to strike (in case of bad sync performance) after validator exit - continue - - threshold = None - reward_share = None - - # FIXME: CHECK default returns [] for key_pivots or [0] ??? - perf_leeway_data.key_pivots.append(UINT256_MAX) - for i, pivot_number in enumerate(perf_leeway_data.key_pivots): - if number <= pivot_number: - threshold = network_perf - perf_leeway_data.performance_leeways[i] / TOTAL_BASIS_POINTS - break - - reward_share_data.key_pivots.append(UINT256_MAX) - for i, pivot_number in enumerate(reward_share_data.key_pivots): - if number <= pivot_number: - reward_share = reward_share_data.reward_shares[i] / TOTAL_BASIS_POINTS - break - - if threshold is None or reward_share is None: - raise ValueError(f"Failed to calculate threshold or reward share for {validator.index=}") - - validator_rebate = self.process_validator_duty( + for key_number, validator in enumerate(sorted_validators): + key_threshold = max(network_perf - perf_leeway.get_for(key_number), 0) + key_reward_share = reward_share.get_for(key_number) + + att_duty = self.state.data[frame].attestations.get(validator.index) + prop_duty = self.state.data[frame].proposals.get(validator.index) + sync_duty = self.state.data[frame].syncs.get(validator.index) + + # TODO: better naming + validator_rebate = self.process_validator_duties( validator, att_duty, prop_duty, sync_duty, - threshold, - reward_share, + key_threshold, + key_reward_share, perf_coeffs, participation_shares, - log_operator + log_operator, ) total_rebate_share += validator_rebate rewards_distribution = self.calc_rewards_distribution_in_frame( - participation_shares, - total_rebate_share, - rewards_to_distribute + participation_shares, total_rebate_share, rewards_to_distribute ) for no_id, no_rewards in rewards_distribution.items(): @@ -167,7 +147,6 @@ def _calculate_distribution_in_frame( return rewards_distribution, log - @lru_cache(maxsize=32) # TODO: any feasible size regard to curves count def _get_curve_params(self, curve_id: int, blockstamp: ReferenceBlockStamp): perf_coeffs = self.w3.csm.params.get_performance_coefficients(curve_id, blockstamp.block_hash) perf_leeway_data = self.w3.csm.params.get_performance_leeway_data(curve_id, blockstamp.block_hash) @@ -178,8 +157,7 @@ def _get_network_performance(self, frame: Frame) -> float: att_perf = self.state.get_att_network_aggr(frame) prop_perf = self.state.get_prop_network_aggr(frame) sync_perf = self.state.get_sync_network_aggr(frame) - network_perf_coeffs = PerformanceCoefficients(attestations_weight=54, blocks_weight=8, sync_weight=2) - network_perf = Distribution.calculate_performance(att_perf, prop_perf, sync_perf, network_perf_coeffs) + network_perf = PerformanceCoefficients().calc_performance(att_perf, prop_perf, sync_perf) return network_perf def _get_stuck_operators(self, frame: Frame, frame_blockstamp: ReferenceBlockStamp) -> set[NodeOperatorId]: @@ -208,17 +186,23 @@ def _get_stuck_operators(self, frame: Frame, frame_blockstamp: ReferenceBlockSta return set(stuck_from_digests) | set(stuck_from_events) @staticmethod - def process_validator_duty( + def process_validator_duties( validator: LidoValidator, - attestation_duty: DutyAccumulator | None, - sync_duty: DutyAccumulator | None, - proposal_duty: DutyAccumulator | None, + attestation: DutyAccumulator | None, + sync: DutyAccumulator | None, + proposal: DutyAccumulator | None, threshold: float, reward_share: float, - performance_coefficients: PerformanceCoefficients, + perf_coeffs: PerformanceCoefficients, participation_shares: defaultdict[NodeOperatorId, int], - log_operator: OperatorFrameSummary + log_operator: OperatorFrameSummary, ) -> int: + if attestation is None: + # It's possible that the validator is not assigned to any duty, hence it's performance + # is not presented in the aggregates (e.g. exited, pending for activation etc). + # TODO: check `sync_aggr` to strike (in case of bad sync performance) after validator exit + return 0 + log_validator = log_operator.validators[validator.index] log_validator.threshold = threshold @@ -230,39 +214,25 @@ def process_validator_duty( log_validator.slashed = True return 0 - performance = Distribution.calculate_performance(attestation_duty, proposal_duty, sync_duty, performance_coefficients) + performance = perf_coeffs.calc_performance(attestation, proposal, sync) log_validator.performance = performance - log_validator.attestation_duty = attestation_duty - log_validator.proposal_duty = proposal_duty - log_validator.sync_duty = sync_duty + log_validator.attestation_duty = attestation + if proposal: + log_validator.proposal_duty = proposal + if sync: + log_validator.sync_duty = sync if performance > threshold: # Count of assigned attestations used as a metrics of time the validator was active in the current frame. # Reward share is a share of the operator's reward the validator should get. It can be less than 1. - participation_share = max(int(attestation_duty.assigned * reward_share), 1) + participation_share = max(int(attestation.assigned * reward_share), 1) participation_shares[validator.lido_id.operatorIndex] += participation_share - rebate = attestation_duty.assigned - participation_share - return rebate + rebate_share = attestation.assigned - participation_share + return rebate_share return 0 - @staticmethod - def calculate_performance(att_aggr, prop_aggr, sync_aggr, coeffs) -> float: - performance = att_aggr.perf - if prop_aggr and sync_aggr: - base = coeffs.attestations_weight + coeffs.blocks_weight + coeffs.sync_weight - performance = coeffs.attestations_weight / base * att_aggr.perf + coeffs.blocks_weight / base * prop_aggr.perf + coeffs.sync_weight / base * sync_aggr.perf - elif prop_aggr: - base = coeffs.attestations_weight + coeffs.blocks_weight - performance = coeffs.attestations_weight / base * att_aggr.perf + coeffs.blocks_weight / base * prop_aggr.perf - elif sync_aggr: - base = coeffs.attestations_weight + coeffs.sync_weight - performance = coeffs.attestations_weight / base * att_aggr.perf + coeffs.sync_weight / base * sync_aggr.perf - if performance > 1: - raise ValueError(f"Invalid performance: {performance=}") - return performance - @staticmethod def calc_rewards_distribution_in_frame( participation_shares: dict[NodeOperatorId, int], @@ -284,4 +254,5 @@ def calc_rewards_distribution_in_frame( def validate_distribution(total_distributed_rewards, total_rebate, total_rewards_to_distribute): if (total_distributed_rewards + total_rebate) > total_rewards_to_distribute: raise ValueError( - f"Invalid distribution: {total_distributed_rewards + total_rebate} > {total_rewards_to_distribute}") + f"Invalid distribution: {total_distributed_rewards + total_rebate} > {total_rewards_to_distribute}" + ) diff --git a/src/modules/csm/log.py b/src/modules/csm/log.py index 85d9caa68..92d0c12e0 100644 --- a/src/modules/csm/log.py +++ b/src/modules/csm/log.py @@ -28,7 +28,7 @@ class ValidatorFrameSummary: class OperatorFrameSummary: distributed: int = 0 stuck: bool = False - performance_coefficients: PerformanceCoefficients = None + performance_coefficients: PerformanceCoefficients = field(default_factory=PerformanceCoefficients) validators: dict[ValidatorIndex, ValidatorFrameSummary] = field(default_factory=lambda: defaultdict(ValidatorFrameSummary)) diff --git a/src/modules/csm/state.py b/src/modules/csm/state.py index aca399c44..7a1e53395 100644 --- a/src/modules/csm/state.py +++ b/src/modules/csm/state.py @@ -34,9 +34,20 @@ def add_duty(self, included: bool) -> None: self.assigned += 1 self.included += 1 if included else 0 + def merge(self, other: 'DutyAccumulator') -> None: + self.assigned += other.assigned + self.included += other.included + + +@dataclass +class Duties: + attestations: defaultdict[ValidatorIndex, DutyAccumulator] + proposals: defaultdict[ValidatorIndex, DutyAccumulator] + syncs: defaultdict[ValidatorIndex, DutyAccumulator] + type Frame = tuple[EpochNumber, EpochNumber] -type StateData = dict[Frame, defaultdict[ValidatorIndex, DutyAccumulator]] +type StateData = dict[Frame, Duties] class State: @@ -50,9 +61,7 @@ class State: The state can be migrated to be used for another frame's report by calling the `migrate` method. """ frames: list[Frame] - att_data: StateData - prop_data: StateData - sync_data: StateData + data: StateData _epochs_to_process: tuple[EpochNumber, ...] _processed_epochs: set[EpochNumber] @@ -61,9 +70,7 @@ class State: def __init__(self) -> None: self.frames = [] - self.att_data = {} - self.prop_data = {} - self.sync_data = {} + self.data = {} self._epochs_to_process = tuple() self._processed_epochs = set() @@ -103,9 +110,7 @@ def buffer(self) -> Path: @property def is_empty(self) -> bool: return ( - not self.att_data and - not self.sync_data and - not self.prop_data and + not self.data and not self._epochs_to_process and not self._processed_epochs ) @@ -129,9 +134,7 @@ def _calculate_frames(epochs_to_process: tuple[EpochNumber, ...], epochs_per_fra return [(frame[0], frame[-1]) for frame in batched(sorted(epochs_to_process), epochs_per_frame)] def clear(self) -> None: - self.att_data = {} - self.sync_data = {} - self.prop_data = {} + self.data = {} self._epochs_to_process = tuple() self._processed_epochs.clear() assert self.is_empty @@ -146,15 +149,15 @@ def find_frame(self, epoch: EpochNumber) -> Frame: def increment_att_duty(self, epoch: EpochNumber, val_index: ValidatorIndex, included: bool) -> None: frame = self.find_frame(epoch) - self.att_data[frame][val_index].add_duty(included) + self.data[frame].attestations[val_index].add_duty(included) def increment_prop_duty(self, epoch: EpochNumber, val_index: ValidatorIndex, included: bool) -> None: frame = self.find_frame(epoch) - self.prop_data[frame][val_index].add_duty(included) + self.data[frame].proposals[val_index].add_duty(included) def increment_sync_duty(self, epoch: EpochNumber, val_index: ValidatorIndex, included: bool) -> None: frame = self.find_frame(epoch) - self.sync_data[frame][val_index].add_duty(included) + self.data[frame].syncs[val_index].add_duty(included) def add_processed_epoch(self, epoch: EpochNumber) -> None: self._processed_epochs.add(epoch) @@ -188,9 +191,13 @@ def migrate( def _migrate_frames_data(self, new_frames: list[Frame]): logger.info({"msg": f"Migrating duties data cache: {self.frames=} -> {new_frames=}"}) - new_att_data: StateData = {frame: defaultdict(DutyAccumulator) for frame in new_frames} - new_prop_data: StateData = {frame: defaultdict(DutyAccumulator) for frame in new_frames} - new_sync_data: StateData = {frame: defaultdict(DutyAccumulator) for frame in new_frames} + new_data: StateData = {} + for frame in new_frames: + new_data[frame] = Duties( + attestations=defaultdict(DutyAccumulator), + proposals=defaultdict(DutyAccumulator), + syncs=defaultdict(DutyAccumulator), + ) def overlaps(a: Frame, b: Frame): return a[0] <= b[0] and a[1] >= b[1] @@ -201,23 +208,20 @@ def overlaps(a: Frame, b: Frame): if overlaps(new_frame, frame_to_consume): assert frame_to_consume not in consumed consumed.append(frame_to_consume) - for val, duty in self.att_data[frame_to_consume].items(): - new_att_data[new_frame][val].assigned += duty.assigned - new_att_data[new_frame][val].included += duty.included - for val, duty in self.prop_data[frame_to_consume].items(): - new_prop_data[new_frame][val].assigned += duty.assigned - new_prop_data[new_frame][val].included += duty.included - for val, duty in self.sync_data[frame_to_consume].items(): - new_sync_data[new_frame][val].assigned += duty.assigned - new_sync_data[new_frame][val].included += duty.included + frame_to_consume_data = self.data[frame_to_consume] + new_frame_data = new_data[new_frame] + for val, duty in frame_to_consume_data.attestations.items(): + new_frame_data.attestations[val].merge(duty) + for val, duty in frame_to_consume_data.proposals.items(): + new_frame_data.proposals[val].merge(duty) + for val, duty in frame_to_consume_data.syncs.items(): + new_frame_data.syncs[val].merge(duty) for frame in self.frames: if frame in consumed: continue logger.warning({"msg": f"Invalidating frame duties data cache: {frame}"}) self._processed_epochs -= set(sequence(*frame)) - self.att_data = new_att_data - self.prop_data = new_prop_data - self.sync_data = new_sync_data + self.data = new_data def validate(self, l_epoch: EpochNumber, r_epoch: EpochNumber) -> None: if not self.is_fulfilled: @@ -233,29 +237,20 @@ def validate(self, l_epoch: EpochNumber, r_epoch: EpochNumber) -> None: def get_att_network_aggr(self, frame: Frame) -> DutyAccumulator: # TODO: exclude `active_slashed` validators from the calculation - frame_data = self.att_data.get(frame) - if frame_data is None: - raise ValueError(f"No data for frame {frame} to calculate network aggregate") - aggr = self.get_duty_network_aggr(frame_data) + aggr = self.get_duty_network_aggr(self.data[frame].attestations) logger.info({"msg": "Network attestations aggregate computed", "value": repr(aggr), "avg_perf": aggr.perf}) return aggr - def get_sync_network_aggr(self, frame: Frame) -> DutyAccumulator: - frame_data = self.sync_data.get(frame) - if frame_data is None: - raise ValueError(f"No data for frame {frame} to calculate syncs network aggregate") - aggr = self.get_duty_network_aggr(frame_data) - logger.info({"msg": "Network syncs aggregate computed", "value": repr(aggr), "avg_perf": aggr.perf}) - return aggr - def get_prop_network_aggr(self, frame: Frame) -> DutyAccumulator: - frame_data = self.prop_data.get(frame) - if frame_data is None: - raise ValueError(f"No data for frame {frame} to calculate proposal network aggregate") - aggr = self.get_duty_network_aggr(frame_data) + aggr = self.get_duty_network_aggr(self.data[frame].proposals) logger.info({"msg": "Network proposal aggregate computed", "value": repr(aggr), "avg_perf": aggr.perf}) return aggr + def get_sync_network_aggr(self, frame: Frame) -> DutyAccumulator: + aggr = self.get_duty_network_aggr(self.data[frame].syncs) + logger.info({"msg": "Network syncs aggregate computed", "value": repr(aggr), "avg_perf": aggr.perf}) + return aggr + @staticmethod def get_duty_network_aggr(duty_frame_data: defaultdict[ValidatorIndex, DutyAccumulator]) -> DutyAccumulator: included = assigned = 0 diff --git a/src/providers/execution/contracts/cs_parameters_registry.py b/src/providers/execution/contracts/cs_parameters_registry.py index 89b910614..f0f0e1946 100644 --- a/src/providers/execution/contracts/cs_parameters_registry.py +++ b/src/providers/execution/contracts/cs_parameters_registry.py @@ -3,6 +3,7 @@ from web3.types import BlockIdentifier +from src.constants import UINT256_MAX, TOTAL_BASIS_POINTS from src.providers.execution.base_interface import ContractInterface logger = logging.getLogger(__name__) @@ -10,9 +11,28 @@ @dataclass class PerformanceCoefficients: - attestations_weight: int - blocks_weight: int - sync_weight: int + attestations_weight: int = 54 + blocks_weight: int = 8 + sync_weight: int = 2 + + def calc_performance(self, att_aggr, prop_aggr, sync_aggr) -> float: + base = self.attestations_weight + performance = att_aggr.perf * self.attestations_weight + + if prop_aggr: + base += self.blocks_weight + performance += prop_aggr.perf * self.blocks_weight + + if sync_aggr: + base += self.sync_weight + performance += sync_aggr.perf * self.sync_weight + + performance /= base + + if performance > 1: + raise ValueError(f"Invalid performance: {performance=}") + + return performance @dataclass @@ -20,12 +40,24 @@ class RewardShare: key_pivots: list[int] reward_shares: list[int] + def get_for(self, key_number: int) -> float: + for i, pivot_number in enumerate(self.key_pivots + [UINT256_MAX]): + if key_number <= pivot_number: + return self.reward_shares[i] / TOTAL_BASIS_POINTS + raise ValueError(f"Key number {key_number} is out of {self.key_pivots}") + @dataclass class PerformanceLeeway: key_pivots: list[int] performance_leeways: list[int] + def get_for(self, key_number: int) -> float: + for i, pivot_number in enumerate(self.key_pivots + [UINT256_MAX]): + if key_number <= pivot_number: + return self.performance_leeways[i] / TOTAL_BASIS_POINTS + raise ValueError(f"Key number {key_number} is out of {self.key_pivots}") + class CSParametersRegistryContract(ContractInterface): abi_path = "./assets/CSParametersRegistry.json" From 78271ca9f6c0d04f8f3f1796831f9392e20e85c6 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Thu, 20 Feb 2025 17:34:58 +0100 Subject: [PATCH 029/162] feat: CSParametersRegistry --- assets/CSFeeOracle.json | 2 +- assets/CSModule.json | 2 +- assets/CSParametersRegistry.json | 1 + .../execution/contracts/cs_accounting.py | 14 +++ .../execution/contracts/cs_module.py | 13 ++ .../contracts/cs_parameters_registry.py | 114 ++++++++++++++++++ src/web3py/extensions/csm.py | 16 ++- 7 files changed, 158 insertions(+), 4 deletions(-) create mode 100644 assets/CSParametersRegistry.json create mode 100644 src/providers/execution/contracts/cs_parameters_registry.py diff --git a/assets/CSFeeOracle.json b/assets/CSFeeOracle.json index 4c0e68d3d..c9bd74471 100644 --- a/assets/CSFeeOracle.json +++ b/assets/CSFeeOracle.json @@ -1 +1 @@ -[{"type":"constructor","inputs":[{"name":"secondsPerSlot","type":"uint256","internalType":"uint256"},{"name":"genesisTime","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"CONTRACT_MANAGER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"GENESIS_TIME","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"MANAGE_CONSENSUS_CONTRACT_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"MANAGE_CONSENSUS_VERSION_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_INFINITELY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RECOVERER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RESUME_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"SECONDS_PER_SLOT","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"SUBMIT_DATA_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"avgPerfLeewayBP","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"discardConsensusReport","inputs":[{"name":"refSlot","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"feeDistributor","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSFeeDistributor"}],"stateMutability":"view"},{"type":"function","name":"getConsensusContract","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getConsensusReport","inputs":[],"outputs":[{"name":"hash","type":"bytes32","internalType":"bytes32"},{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"processingDeadlineTime","type":"uint256","internalType":"uint256"},{"name":"processingStarted","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"getConsensusVersion","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getContractVersion","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getLastProcessingRefSlot","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getResumeSinceTimestamp","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"admin","type":"address","internalType":"address"},{"name":"feeDistributorContract","type":"address","internalType":"address"},{"name":"consensusContract","type":"address","internalType":"address"},{"name":"consensusVersion","type":"uint256","internalType":"uint256"},{"name":"_avgPerfLeewayBP","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"isPaused","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"pauseFor","inputs":[{"name":"duration","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"pauseUntil","inputs":[{"name":"pauseUntilInclusive","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC1155","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC20","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC721","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverEther","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resume","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setConsensusContract","inputs":[{"name":"addr","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setConsensusVersion","inputs":[{"name":"version","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setFeeDistributorContract","inputs":[{"name":"feeDistributorContract","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setPerformanceLeeway","inputs":[{"name":"valueBP","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"submitConsensusReport","inputs":[{"name":"reportHash","type":"bytes32","internalType":"bytes32"},{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"submitReportData","inputs":[{"name":"data","type":"tuple","internalType":"struct CSFeeOracle.ReportData","components":[{"name":"consensusVersion","type":"uint256","internalType":"uint256"},{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"treeRoot","type":"bytes32","internalType":"bytes32"},{"name":"treeCid","type":"string","internalType":"string"},{"name":"logCid","type":"string","internalType":"string"},{"name":"distributed","type":"uint256","internalType":"uint256"}]},{"name":"contractVersion","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"event","name":"ConsensusHashContractSet","inputs":[{"name":"addr","type":"address","indexed":true,"internalType":"address"},{"name":"prevAddr","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"ConsensusVersionSet","inputs":[{"name":"version","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"prevVersion","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ContractVersionSet","inputs":[{"name":"version","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC1155Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC20Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC721Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"EtherRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"FeeDistributorContractSet","inputs":[{"name":"feeDistributorContract","type":"address","indexed":false,"internalType":"address"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"Paused","inputs":[{"name":"duration","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PerfLeewaySet","inputs":[{"name":"valueBP","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ProcessingStarted","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"hash","type":"bytes32","indexed":false,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"ReportDiscarded","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"hash","type":"bytes32","indexed":false,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"ReportSettled","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ReportSubmitted","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"hash","type":"bytes32","indexed":false,"internalType":"bytes32"},{"name":"processingDeadlineTime","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Resumed","inputs":[],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"StETHSharesRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"WarnProcessingMissed","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"AddressCannotBeSame","inputs":[]},{"type":"error","name":"AddressCannotBeZero","inputs":[]},{"type":"error","name":"FailedToSendEther","inputs":[]},{"type":"error","name":"HashCannotBeZero","inputs":[]},{"type":"error","name":"InitialRefSlotCannotBeLessThanProcessingOne","inputs":[{"name":"initialRefSlot","type":"uint256","internalType":"uint256"},{"name":"processingRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"InvalidContractVersion","inputs":[]},{"type":"error","name":"InvalidContractVersionIncrement","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidPerfLeeway","inputs":[]},{"type":"error","name":"NoConsensusReportToProcess","inputs":[]},{"type":"error","name":"NonZeroContractVersionOnInit","inputs":[]},{"type":"error","name":"NotAllowedToRecover","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"PauseUntilMustBeInFuture","inputs":[]},{"type":"error","name":"PausedExpected","inputs":[]},{"type":"error","name":"ProcessingDeadlineMissed","inputs":[{"name":"deadline","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"RefSlotAlreadyProcessing","inputs":[]},{"type":"error","name":"RefSlotCannotDecrease","inputs":[{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"prevRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"RefSlotMustBeGreaterThanProcessingOne","inputs":[{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"processingRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"ResumedExpected","inputs":[]},{"type":"error","name":"SafeCastOverflowedUintDowncast","inputs":[{"name":"bits","type":"uint8","internalType":"uint8"},{"name":"value","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"SecondsPerSlotCannotBeZero","inputs":[]},{"type":"error","name":"SenderIsNotTheConsensusContract","inputs":[]},{"type":"error","name":"SenderNotAllowed","inputs":[]},{"type":"error","name":"UnexpectedChainConfig","inputs":[]},{"type":"error","name":"UnexpectedConsensusVersion","inputs":[{"name":"expectedVersion","type":"uint256","internalType":"uint256"},{"name":"receivedVersion","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"UnexpectedContractVersion","inputs":[{"name":"expected","type":"uint256","internalType":"uint256"},{"name":"received","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"UnexpectedDataHash","inputs":[{"name":"consensusHash","type":"bytes32","internalType":"bytes32"},{"name":"receivedHash","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"UnexpectedRefSlot","inputs":[{"name":"consensusRefSlot","type":"uint256","internalType":"uint256"},{"name":"dataRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"VersionCannotBeSame","inputs":[]},{"type":"error","name":"VersionCannotBeZero","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroFeeDistributorAddress","inputs":[]},{"type":"error","name":"ZeroPauseDuration","inputs":[]}] +[{"type":"constructor","inputs":[{"name":"secondsPerSlot","type":"uint256","internalType":"uint256"},{"name":"genesisTime","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"GENESIS_TIME","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"MANAGE_CONSENSUS_CONTRACT_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"MANAGE_CONSENSUS_VERSION_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_INFINITELY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RECOVERER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RESUME_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"SECONDS_PER_SLOT","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"SUBMIT_DATA_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"discardConsensusReport","inputs":[{"name":"refSlot","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"feeDistributor","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSFeeDistributor"}],"stateMutability":"view"},{"type":"function","name":"finalizeUpgradeV2","inputs":[{"name":"consensusVersion","type":"uint256","internalType":"uint256"},{"name":"strikesContract","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"getConsensusContract","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getConsensusReport","inputs":[],"outputs":[{"name":"hash","type":"bytes32","internalType":"bytes32"},{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"processingDeadlineTime","type":"uint256","internalType":"uint256"},{"name":"processingStarted","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"getConsensusVersion","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getContractVersion","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getLastProcessingRefSlot","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getResumeSinceTimestamp","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"admin","type":"address","internalType":"address"},{"name":"feeDistributorContract","type":"address","internalType":"address"},{"name":"strikesContract","type":"address","internalType":"address"},{"name":"consensusContract","type":"address","internalType":"address"},{"name":"consensusVersion","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"isPaused","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"pauseFor","inputs":[{"name":"duration","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"pauseUntil","inputs":[{"name":"pauseUntilInclusive","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC1155","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC20","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC721","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverEther","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resume","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setConsensusContract","inputs":[{"name":"addr","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setConsensusVersion","inputs":[{"name":"version","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setFeeDistributorContract","inputs":[{"name":"feeDistributorContract","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setStrikesContract","inputs":[{"name":"strikesContract","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"strikes","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSStrikes"}],"stateMutability":"view"},{"type":"function","name":"submitConsensusReport","inputs":[{"name":"reportHash","type":"bytes32","internalType":"bytes32"},{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"submitReportData","inputs":[{"name":"data","type":"tuple","internalType":"struct ICSFeeOracle.ReportData","components":[{"name":"consensusVersion","type":"uint256","internalType":"uint256"},{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"treeRoot","type":"bytes32","internalType":"bytes32"},{"name":"treeCid","type":"string","internalType":"string"},{"name":"logCid","type":"string","internalType":"string"},{"name":"distributed","type":"uint256","internalType":"uint256"},{"name":"rebate","type":"uint256","internalType":"uint256"},{"name":"strikesTreeRoot","type":"bytes32","internalType":"bytes32"},{"name":"strikesTreeCid","type":"string","internalType":"string"}]},{"name":"contractVersion","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"event","name":"ConsensusHashContractSet","inputs":[{"name":"addr","type":"address","indexed":true,"internalType":"address"},{"name":"prevAddr","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"ConsensusVersionSet","inputs":[{"name":"version","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"prevVersion","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ContractVersionSet","inputs":[{"name":"version","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC1155Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC20Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC721Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"EtherRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"FeeDistributorContractSet","inputs":[{"name":"feeDistributorContract","type":"address","indexed":false,"internalType":"address"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"Paused","inputs":[{"name":"duration","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ProcessingStarted","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"hash","type":"bytes32","indexed":false,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"ReportDiscarded","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"hash","type":"bytes32","indexed":false,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"ReportSubmitted","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"hash","type":"bytes32","indexed":false,"internalType":"bytes32"},{"name":"processingDeadlineTime","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Resumed","inputs":[],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"StETHSharesRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"StrikesContractSet","inputs":[{"name":"strikesContract","type":"address","indexed":false,"internalType":"address"}],"anonymous":false},{"type":"event","name":"WarnProcessingMissed","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"AddressCannotBeSame","inputs":[]},{"type":"error","name":"AddressCannotBeZero","inputs":[]},{"type":"error","name":"FailedToSendEther","inputs":[]},{"type":"error","name":"HashCannotBeZero","inputs":[]},{"type":"error","name":"InitialRefSlotCannotBeLessThanProcessingOne","inputs":[{"name":"initialRefSlot","type":"uint256","internalType":"uint256"},{"name":"processingRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"InvalidContractVersion","inputs":[]},{"type":"error","name":"InvalidContractVersionIncrement","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"NoConsensusReportToProcess","inputs":[]},{"type":"error","name":"NonZeroContractVersionOnInit","inputs":[]},{"type":"error","name":"NotAllowedToRecover","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"PauseUntilMustBeInFuture","inputs":[]},{"type":"error","name":"PausedExpected","inputs":[]},{"type":"error","name":"ProcessingDeadlineMissed","inputs":[{"name":"deadline","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"RefSlotAlreadyProcessing","inputs":[]},{"type":"error","name":"RefSlotCannotDecrease","inputs":[{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"prevRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"RefSlotMustBeGreaterThanProcessingOne","inputs":[{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"processingRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"ResumedExpected","inputs":[]},{"type":"error","name":"SafeCastOverflowedUintDowncast","inputs":[{"name":"bits","type":"uint8","internalType":"uint8"},{"name":"value","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"SecondsPerSlotCannotBeZero","inputs":[]},{"type":"error","name":"SenderIsNotTheConsensusContract","inputs":[]},{"type":"error","name":"SenderNotAllowed","inputs":[]},{"type":"error","name":"UnexpectedChainConfig","inputs":[]},{"type":"error","name":"UnexpectedConsensusVersion","inputs":[{"name":"expectedVersion","type":"uint256","internalType":"uint256"},{"name":"receivedVersion","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"UnexpectedContractVersion","inputs":[{"name":"expected","type":"uint256","internalType":"uint256"},{"name":"received","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"UnexpectedDataHash","inputs":[{"name":"consensusHash","type":"bytes32","internalType":"bytes32"},{"name":"receivedHash","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"UnexpectedRefSlot","inputs":[{"name":"consensusRefSlot","type":"uint256","internalType":"uint256"},{"name":"dataRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"VersionCannotBeSame","inputs":[]},{"type":"error","name":"VersionCannotBeZero","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroFeeDistributorAddress","inputs":[]},{"type":"error","name":"ZeroPauseDuration","inputs":[]},{"type":"error","name":"ZeroStrikesAddress","inputs":[]}] \ No newline at end of file diff --git a/assets/CSModule.json b/assets/CSModule.json index 02b75373e..53518035f 100644 --- a/assets/CSModule.json +++ b/assets/CSModule.json @@ -1 +1 @@ -[{"type":"constructor","inputs":[{"name":"moduleType","type":"bytes32","internalType":"bytes32"},{"name":"minSlashingPenaltyQuotient","type":"uint256","internalType":"uint256"},{"name":"elRewardsStealingFine","type":"uint256","internalType":"uint256"},{"name":"maxKeysPerOperatorEA","type":"uint256","internalType":"uint256"},{"name":"lidoLocator","type":"address","internalType":"address"}],"stateMutability":"nonpayable"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"EL_REWARDS_STEALING_FINE","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"INITIAL_SLASHING_PENALTY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"LIDO_LOCATOR","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ILidoLocator"}],"stateMutability":"view"},{"type":"function","name":"MAX_SIGNING_KEYS_PER_OPERATOR_BEFORE_PUBLIC_RELEASE","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"MODULE_MANAGER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_INFINITELY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RECOVERER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"REPORT_EL_REWARDS_STEALING_PENALTY_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RESUME_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"SETTLE_EL_REWARDS_STEALING_PENALTY_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"STAKING_ROUTER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"STETH","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IStETH"}],"stateMutability":"view"},{"type":"function","name":"VERIFIER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"accounting","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSAccounting"}],"stateMutability":"view"},{"type":"function","name":"activatePublicRelease","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"addNodeOperatorETH","inputs":[{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"},{"name":"managerAddress","type":"address","internalType":"address"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"eaProof","type":"bytes32[]","internalType":"bytes32[]"},{"name":"referrer","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"addNodeOperatorStETH","inputs":[{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"},{"name":"managerAddress","type":"address","internalType":"address"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]},{"name":"eaProof","type":"bytes32[]","internalType":"bytes32[]"},{"name":"referrer","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"addNodeOperatorWstETH","inputs":[{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"},{"name":"managerAddress","type":"address","internalType":"address"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]},{"name":"eaProof","type":"bytes32[]","internalType":"bytes32[]"},{"name":"referrer","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"addValidatorKeysETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"addValidatorKeysStETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"addValidatorKeysWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"cancelELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"claimRewardsStETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stETHAmount","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"claimRewardsUnstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stEthAmount","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"claimRewardsWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"wstETHAmount","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"cleanDepositQueue","inputs":[{"name":"maxItems","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"compensateELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"confirmNodeOperatorManagerAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"confirmNodeOperatorRewardAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"decreaseVettedSigningKeysCount","inputs":[{"name":"nodeOperatorIds","type":"bytes","internalType":"bytes"},{"name":"vettedSigningKeysCounts","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"depositETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"depositQueue","inputs":[],"outputs":[{"name":"head","type":"uint128","internalType":"uint128"},{"name":"length","type":"uint128","internalType":"uint128"}],"stateMutability":"view"},{"type":"function","name":"depositQueueItem","inputs":[{"name":"index","type":"uint128","internalType":"uint128"}],"outputs":[{"name":"","type":"uint256","internalType":"Batch"}],"stateMutability":"view"},{"type":"function","name":"depositStETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stETHAmount","type":"uint256","internalType":"uint256"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"depositWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"wstETHAmount","type":"uint256","internalType":"uint256"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"earlyAdoption","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSEarlyAdoption"}],"stateMutability":"view"},{"type":"function","name":"getActiveNodeOperatorsCount","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperator","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct NodeOperator","components":[{"name":"totalAddedKeys","type":"uint32","internalType":"uint32"},{"name":"totalWithdrawnKeys","type":"uint32","internalType":"uint32"},{"name":"totalDepositedKeys","type":"uint32","internalType":"uint32"},{"name":"totalVettedKeys","type":"uint32","internalType":"uint32"},{"name":"stuckValidatorsCount","type":"uint32","internalType":"uint32"},{"name":"depositableValidatorsCount","type":"uint32","internalType":"uint32"},{"name":"targetLimit","type":"uint32","internalType":"uint32"},{"name":"targetLimitMode","type":"uint8","internalType":"uint8"},{"name":"totalExitedKeys","type":"uint32","internalType":"uint32"},{"name":"enqueuedCount","type":"uint32","internalType":"uint32"},{"name":"managerAddress","type":"address","internalType":"address"},{"name":"proposedManagerAddress","type":"address","internalType":"address"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"proposedRewardAddress","type":"address","internalType":"address"}]}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorIds","inputs":[{"name":"offset","type":"uint256","internalType":"uint256"},{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"nodeOperatorIds","type":"uint256[]","internalType":"uint256[]"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorIsActive","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorNonWithdrawnKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorSummary","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"targetLimitMode","type":"uint256","internalType":"uint256"},{"name":"targetValidatorsCount","type":"uint256","internalType":"uint256"},{"name":"stuckValidatorsCount","type":"uint256","internalType":"uint256"},{"name":"refundedValidatorsCount","type":"uint256","internalType":"uint256"},{"name":"stuckPenaltyEndTimestamp","type":"uint256","internalType":"uint256"},{"name":"totalExitedValidators","type":"uint256","internalType":"uint256"},{"name":"totalDepositedValidators","type":"uint256","internalType":"uint256"},{"name":"depositableValidatorsCount","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorsCount","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNonce","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getResumeSinceTimestamp","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getSigningKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"startIndex","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bytes","internalType":"bytes"}],"stateMutability":"view"},{"type":"function","name":"getSigningKeysWithSignatures","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"startIndex","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"keys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"}],"stateMutability":"view"},{"type":"function","name":"getStakingModuleSummary","inputs":[],"outputs":[{"name":"totalExitedValidators","type":"uint256","internalType":"uint256"},{"name":"totalDepositedValidators","type":"uint256","internalType":"uint256"},{"name":"depositableValidatorsCount","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getType","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"_accounting","type":"address","internalType":"address"},{"name":"_earlyAdoption","type":"address","internalType":"address"},{"name":"_keyRemovalCharge","type":"uint256","internalType":"uint256"},{"name":"admin","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"isPaused","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"isValidatorSlashed","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"isValidatorWithdrawn","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"keyRemovalCharge","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"normalizeQueue","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"obtainDepositData","inputs":[{"name":"depositsCount","type":"uint256","internalType":"uint256"},{"name":"","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"}],"stateMutability":"nonpayable"},{"type":"function","name":"onExitedAndStuckValidatorsCountsUpdated","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"onRewardsMinted","inputs":[{"name":"totalShares","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"onWithdrawalCredentialsChanged","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"pauseFor","inputs":[{"name":"duration","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"proposeNodeOperatorManagerAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"proposedAddress","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"proposeNodeOperatorRewardAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"proposedAddress","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"publicRelease","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"recoverERC1155","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC20","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC721","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverEther","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverStETHShares","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"removeKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"startIndex","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"reportELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"blockHash","type":"bytes32","internalType":"bytes32"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resetNodeOperatorManagerAddress","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resume","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setKeyRemovalCharge","inputs":[{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"settleELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorIds","type":"uint256[]","internalType":"uint256[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"submitInitialSlashing","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"submitWithdrawal","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"unsafeUpdateValidatorsCount","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"exitedValidatorsKeysCount","type":"uint256","internalType":"uint256"},{"name":"stuckValidatorsKeysCount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateExitedValidatorsCount","inputs":[{"name":"nodeOperatorIds","type":"bytes","internalType":"bytes"},{"name":"exitedValidatorsCounts","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateRefundedValidatorsCount","inputs":[{"name":"","type":"uint256","internalType":"uint256"},{"name":"","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateStuckValidatorsCount","inputs":[{"name":"nodeOperatorIds","type":"bytes","internalType":"bytes"},{"name":"stuckValidatorsCounts","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateTargetValidatorsLimits","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"targetLimitMode","type":"uint256","internalType":"uint256"},{"name":"targetLimit","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"DepositedSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"depositedKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltyCancelled","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltyReported","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"proposedBlockHash","type":"bytes32","indexed":false,"internalType":"bytes32"},{"name":"stolenAmount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltySettled","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ExitedSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"exitedKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"InitialSlashingSubmitted","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"keyIndex","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"KeyRemovalChargeApplied","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"KeyRemovalChargeSet","inputs":[{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"NodeOperatorAdded","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"managerAddress","type":"address","indexed":true,"internalType":"address"},{"name":"rewardAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NonceChanged","inputs":[{"name":"nonce","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Paused","inputs":[{"name":"duration","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PublicRelease","inputs":[],"anonymous":false},{"type":"event","name":"ReferrerSet","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"referrer","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"Resumed","inputs":[],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"SigningKeyAdded","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"SigningKeyAdded","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"SigningKeyRemoved","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"SigningKeyRemoved","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"StuckSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"stuckKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"TargetValidatorsCountChangedByRequest","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"targetLimitMode","type":"uint8","indexed":false,"internalType":"uint8"},{"name":"targetValidatorsCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"TotalSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"totalKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"VettedSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"vettedKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"WithdrawalSubmitted","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"keyIndex","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"AlreadySet","inputs":[]},{"type":"error","name":"AlreadySubmitted","inputs":[]},{"type":"error","name":"AlreadyWithdrawn","inputs":[]},{"type":"error","name":"EmptyKey","inputs":[]},{"type":"error","name":"ExitedKeysDecrease","inputs":[]},{"type":"error","name":"ExitedKeysHigherThanTotalDeposited","inputs":[]},{"type":"error","name":"InvalidAmount","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidKeysCount","inputs":[]},{"type":"error","name":"InvalidLength","inputs":[]},{"type":"error","name":"InvalidReportData","inputs":[]},{"type":"error","name":"InvalidVetKeysPointer","inputs":[]},{"type":"error","name":"MaxSigningKeysCountExceeded","inputs":[]},{"type":"error","name":"NodeOperatorDoesNotExist","inputs":[]},{"type":"error","name":"NotAllowedToJoinYet","inputs":[]},{"type":"error","name":"NotAllowedToRecover","inputs":[]},{"type":"error","name":"NotEnoughKeys","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"NotSupported","inputs":[]},{"type":"error","name":"PauseUntilMustBeInFuture","inputs":[]},{"type":"error","name":"PausedExpected","inputs":[]},{"type":"error","name":"QueueIsEmpty","inputs":[]},{"type":"error","name":"ResumedExpected","inputs":[]},{"type":"error","name":"SenderIsNotEligible","inputs":[]},{"type":"error","name":"SigningKeysInvalidOffset","inputs":[]},{"type":"error","name":"StuckKeysHigherThanExited","inputs":[]},{"type":"error","name":"ZeroAccountingAddress","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroLocatorAddress","inputs":[]},{"type":"error","name":"ZeroPauseDuration","inputs":[]}] +[{"type":"constructor","inputs":[{"name":"moduleType","type":"bytes32","internalType":"bytes32"},{"name":"lidoLocator","type":"address","internalType":"address"},{"name":"parametersRegistry","type":"address","internalType":"address"}],"stateMutability":"nonpayable"},{"type":"function","name":"BAD_PERFORMER_EJECTOR_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"CREATE_NODE_OPERATOR_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"LIDO_LOCATOR","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ILidoLocator"}],"stateMutability":"view"},{"type":"function","name":"PARAMETERS_REGISTRY","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSParametersRegistry"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_INFINITELY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RECOVERER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"REPORT_EL_REWARDS_STEALING_PENALTY_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RESUME_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"SETTLE_EL_REWARDS_STEALING_PENALTY_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"SET_BOND_CURVE_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"STAKING_ROUTER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"STETH","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IStETH"}],"stateMutability":"view"},{"type":"function","name":"VERIFIER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"accounting","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSAccounting"}],"stateMutability":"view"},{"type":"function","name":"addValidatorKeysETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"addValidatorKeysStETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"addValidatorKeysWstETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"cancelELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"changeNodeOperatorRewardAddress","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"newAddress","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"claimRewardsStETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stETHAmount","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"claimedShares","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"claimRewardsUnstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stEthAmount","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"requestId","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"claimRewardsWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"wstETHAmount","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"claimedWstETHAmount","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"cleanDepositQueue","inputs":[{"name":"maxItems","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"removed","type":"uint256","internalType":"uint256"},{"name":"lastRemovedAtDepth","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"compensateELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"confirmNodeOperatorManagerAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"confirmNodeOperatorRewardAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"createNodeOperator","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"managementProperties","type":"tuple","internalType":"struct NodeOperatorManagementProperties","components":[{"name":"managerAddress","type":"address","internalType":"address"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"extendedManagerPermissions","type":"bool","internalType":"bool"}]},{"name":"referrer","type":"address","internalType":"address"}],"outputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"decreaseVettedSigningKeysCount","inputs":[{"name":"nodeOperatorIds","type":"bytes","internalType":"bytes"},{"name":"vettedSigningKeysCounts","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"depositETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"depositQueue","inputs":[],"outputs":[{"name":"head","type":"uint128","internalType":"uint128"},{"name":"tail","type":"uint128","internalType":"uint128"}],"stateMutability":"view"},{"type":"function","name":"depositQueueItem","inputs":[{"name":"index","type":"uint128","internalType":"uint128"}],"outputs":[{"name":"","type":"uint256","internalType":"Batch"}],"stateMutability":"view"},{"type":"function","name":"depositStETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stETHAmount","type":"uint256","internalType":"uint256"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"depositWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"wstETHAmount","type":"uint256","internalType":"uint256"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"ejectBadPerformer","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"},{"name":"strikesCount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"enqueueNodeOperatorKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"finalizeUpgradeV2","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"getActiveNodeOperatorsCount","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperator","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct NodeOperator","components":[{"name":"totalAddedKeys","type":"uint32","internalType":"uint32"},{"name":"totalWithdrawnKeys","type":"uint32","internalType":"uint32"},{"name":"totalDepositedKeys","type":"uint32","internalType":"uint32"},{"name":"totalVettedKeys","type":"uint32","internalType":"uint32"},{"name":"stuckValidatorsCount","type":"uint32","internalType":"uint32"},{"name":"depositableValidatorsCount","type":"uint32","internalType":"uint32"},{"name":"targetLimit","type":"uint32","internalType":"uint32"},{"name":"targetLimitMode","type":"uint8","internalType":"uint8"},{"name":"totalExitedKeys","type":"uint32","internalType":"uint32"},{"name":"enqueuedCount","type":"uint32","internalType":"uint32"},{"name":"managerAddress","type":"address","internalType":"address"},{"name":"proposedManagerAddress","type":"address","internalType":"address"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"proposedRewardAddress","type":"address","internalType":"address"},{"name":"extendedManagerPermissions","type":"bool","internalType":"bool"}]}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorIds","inputs":[{"name":"offset","type":"uint256","internalType":"uint256"},{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"nodeOperatorIds","type":"uint256[]","internalType":"uint256[]"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorIsActive","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorNonWithdrawnKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorSummary","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"targetLimitMode","type":"uint256","internalType":"uint256"},{"name":"targetValidatorsCount","type":"uint256","internalType":"uint256"},{"name":"stuckValidatorsCount","type":"uint256","internalType":"uint256"},{"name":"refundedValidatorsCount","type":"uint256","internalType":"uint256"},{"name":"stuckPenaltyEndTimestamp","type":"uint256","internalType":"uint256"},{"name":"totalExitedValidators","type":"uint256","internalType":"uint256"},{"name":"totalDepositedValidators","type":"uint256","internalType":"uint256"},{"name":"depositableValidatorsCount","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorsCount","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNonce","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getResumeSinceTimestamp","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getSigningKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"startIndex","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bytes","internalType":"bytes"}],"stateMutability":"view"},{"type":"function","name":"getSigningKeysWithSignatures","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"startIndex","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"keys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"}],"stateMutability":"view"},{"type":"function","name":"getStakingModuleSummary","inputs":[],"outputs":[{"name":"totalExitedValidators","type":"uint256","internalType":"uint256"},{"name":"totalDepositedValidators","type":"uint256","internalType":"uint256"},{"name":"depositableValidatorsCount","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getType","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"_accounting","type":"address","internalType":"address"},{"name":"admin","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"isPaused","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"isValidatorEjected","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"isValidatorWithdrawn","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"obtainDepositData","inputs":[{"name":"depositsCount","type":"uint256","internalType":"uint256"},{"name":"","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"}],"stateMutability":"nonpayable"},{"type":"function","name":"onExitedAndStuckValidatorsCountsUpdated","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"onRewardsMinted","inputs":[{"name":"totalShares","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"onWithdrawalCredentialsChanged","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"pauseFor","inputs":[{"name":"duration","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"proposeNodeOperatorManagerAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"proposedAddress","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"proposeNodeOperatorRewardAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"proposedAddress","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"publicRelease","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"recoverERC1155","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC20","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC721","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverEther","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"removeKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"startIndex","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"reportELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"blockHash","type":"bytes32","internalType":"bytes32"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resetNodeOperatorManagerAddress","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resume","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setBondCurve","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"settleELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorIds","type":"uint256[]","internalType":"uint256[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"submitWithdrawal","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"},{"name":"isSlashed","type":"bool","internalType":"bool"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"unsafeUpdateValidatorsCount","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"exitedValidatorsKeysCount","type":"uint256","internalType":"uint256"},{"name":"stuckValidatorsKeysCount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateExitedValidatorsCount","inputs":[{"name":"nodeOperatorIds","type":"bytes","internalType":"bytes"},{"name":"exitedValidatorsCounts","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateRefundedValidatorsCount","inputs":[{"name":"","type":"uint256","internalType":"uint256"},{"name":"","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"view"},{"type":"function","name":"updateStuckValidatorsCount","inputs":[{"name":"nodeOperatorIds","type":"bytes","internalType":"bytes"},{"name":"stuckValidatorsCounts","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateTargetValidatorsLimits","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"targetLimitMode","type":"uint256","internalType":"uint256"},{"name":"targetLimit","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"BatchEnqueued","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"count","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DepositableSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"depositableKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DepositedSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"depositedKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltyCancelled","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltyCompensated","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltyReported","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"proposedBlockHash","type":"bytes32","indexed":false,"internalType":"bytes32"},{"name":"stolenAmount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltySettled","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC1155Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC20Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC721Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"EjectionSubmitted","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"keyIndex","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"EtherRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ExitedSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"exitedKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"KeyRemovalChargeApplied","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"NodeOperatorAdded","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"managerAddress","type":"address","indexed":true,"internalType":"address"},{"name":"rewardAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NodeOperatorManagerAddressChangeProposed","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"oldProposedAddress","type":"address","indexed":true,"internalType":"address"},{"name":"newProposedAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NodeOperatorManagerAddressChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"oldAddress","type":"address","indexed":true,"internalType":"address"},{"name":"newAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NodeOperatorRewardAddressChangeProposed","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"oldProposedAddress","type":"address","indexed":true,"internalType":"address"},{"name":"newProposedAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NodeOperatorRewardAddressChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"oldAddress","type":"address","indexed":true,"internalType":"address"},{"name":"newAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NonceChanged","inputs":[{"name":"nonce","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Paused","inputs":[{"name":"duration","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ReferrerSet","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"referrer","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"Resumed","inputs":[],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"SigningKeyAdded","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"SigningKeyRemoved","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"StETHSharesRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"StuckSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"stuckKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"TargetValidatorsCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"targetLimitMode","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"targetValidatorsCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"TotalSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"totalKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"VettedSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"vettedKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"VettedSigningKeysCountDecreased","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"WithdrawalSubmitted","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"keyIndex","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"AlreadyEjected","inputs":[]},{"type":"error","name":"AlreadyProposed","inputs":[]},{"type":"error","name":"AlreadyWithdrawn","inputs":[]},{"type":"error","name":"EmptyKey","inputs":[]},{"type":"error","name":"ExitedKeysDecrease","inputs":[]},{"type":"error","name":"ExitedKeysHigherThanTotalDeposited","inputs":[]},{"type":"error","name":"FailedToSendEther","inputs":[]},{"type":"error","name":"InvalidAmount","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidInput","inputs":[]},{"type":"error","name":"InvalidKeysCount","inputs":[]},{"type":"error","name":"InvalidLength","inputs":[]},{"type":"error","name":"InvalidReportData","inputs":[]},{"type":"error","name":"InvalidVetKeysPointer","inputs":[]},{"type":"error","name":"MethodCallIsNotAllowed","inputs":[]},{"type":"error","name":"NodeOperatorDoesNotExist","inputs":[]},{"type":"error","name":"NodeOperatorHasKeys","inputs":[]},{"type":"error","name":"NotAllowedToRecover","inputs":[]},{"type":"error","name":"NotEnoughKeys","inputs":[]},{"type":"error","name":"NotEnoughStrikesToEject","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"NotSupported","inputs":[]},{"type":"error","name":"PauseUntilMustBeInFuture","inputs":[]},{"type":"error","name":"PausedExpected","inputs":[]},{"type":"error","name":"QueueIsEmpty","inputs":[]},{"type":"error","name":"QueueLookupNoLimit","inputs":[]},{"type":"error","name":"ResumedExpected","inputs":[]},{"type":"error","name":"SameAddress","inputs":[]},{"type":"error","name":"SenderIsNotEligible","inputs":[]},{"type":"error","name":"SenderIsNotManagerAddress","inputs":[]},{"type":"error","name":"SenderIsNotProposedAddress","inputs":[]},{"type":"error","name":"SenderIsNotRewardAddress","inputs":[]},{"type":"error","name":"SigningKeysInvalidOffset","inputs":[]},{"type":"error","name":"StuckKeysHigherThanNonExited","inputs":[]},{"type":"error","name":"ZeroAccountingAddress","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroLocatorAddress","inputs":[]},{"type":"error","name":"ZeroParametersRegistryAddress","inputs":[]},{"type":"error","name":"ZeroPauseDuration","inputs":[]},{"type":"error","name":"ZeroRewardAddress","inputs":[]},{"type":"error","name":"ZeroSenderAddress","inputs":[]}] \ No newline at end of file diff --git a/assets/CSParametersRegistry.json b/assets/CSParametersRegistry.json new file mode 100644 index 000000000..7db794b4f --- /dev/null +++ b/assets/CSParametersRegistry.json @@ -0,0 +1 @@ +[{"type":"constructor","inputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"defaultBadPerformancePenalty","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultElRewardsStealingAdditionalFine","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultKeyRemovalCharge","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultPerformanceCoefficients","inputs":[],"outputs":[{"name":"attestationsWeight","type":"uint32","internalType":"uint32"},{"name":"blocksWeight","type":"uint32","internalType":"uint32"},{"name":"syncWeight","type":"uint32","internalType":"uint32"}],"stateMutability":"view"},{"type":"function","name":"defaultPerformanceLeeway","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultPriorityQueueLimit","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultRewardShare","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultStrikesParams","inputs":[],"outputs":[{"name":"lifetime","type":"uint32","internalType":"uint32"},{"name":"threshold","type":"uint32","internalType":"uint32"}],"stateMutability":"view"},{"type":"function","name":"getBadPerformancePenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"penalty","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getElRewardsStealingAdditionalFine","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"fine","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getKeyRemovalCharge","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"keyRemovalCharge","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getPerformanceCoefficients","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"attestationsWeight","type":"uint256","internalType":"uint256"},{"name":"blocksWeight","type":"uint256","internalType":"uint256"},{"name":"syncWeight","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getPerformanceLeewayData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"keyPivots","type":"uint256[]","internalType":"uint256[]"},{"name":"performanceLeeways","type":"uint256[]","internalType":"uint256[]"}],"stateMutability":"view"},{"type":"function","name":"getPriorityQueueLimit","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"limit","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRewardShareData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"keyPivots","type":"uint256[]","internalType":"uint256[]"},{"name":"rewardShares","type":"uint256[]","internalType":"uint256[]"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getStrikesParams","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"lifetime","type":"uint256","internalType":"uint256"},{"name":"threshold","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"admin","type":"address","internalType":"address"},{"name":"data","type":"tuple","internalType":"struct ICSParametersRegistry.InitializationData","components":[{"name":"keyRemovalCharge","type":"uint256","internalType":"uint256"},{"name":"elRewardsStealingAdditionalFine","type":"uint256","internalType":"uint256"},{"name":"priorityQueueLimit","type":"uint256","internalType":"uint256"},{"name":"rewardShare","type":"uint256","internalType":"uint256"},{"name":"performanceLeeway","type":"uint256","internalType":"uint256"},{"name":"strikesLifetime","type":"uint256","internalType":"uint256"},{"name":"strikesThreshold","type":"uint256","internalType":"uint256"},{"name":"badPerformancePenalty","type":"uint256","internalType":"uint256"},{"name":"attestationsWeight","type":"uint256","internalType":"uint256"},{"name":"blocksWeight","type":"uint256","internalType":"uint256"},{"name":"syncWeight","type":"uint256","internalType":"uint256"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setBadPerformancePenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"penalty","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultBadPerformancePenalty","inputs":[{"name":"penalty","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultElRewardsStealingAdditionalFine","inputs":[{"name":"fine","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultKeyRemovalCharge","inputs":[{"name":"keyRemovalCharge","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultPerformanceCoefficients","inputs":[{"name":"attestationsWeight","type":"uint256","internalType":"uint256"},{"name":"blocksWeight","type":"uint256","internalType":"uint256"},{"name":"syncWeight","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultPerformanceLeeway","inputs":[{"name":"leeway","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultPriorityQueueLimit","inputs":[{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultRewardShare","inputs":[{"name":"share","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultStrikesParams","inputs":[{"name":"lifetime","type":"uint256","internalType":"uint256"},{"name":"threshold","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setElRewardsStealingAdditionalFine","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"fine","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setKeyRemovalCharge","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"keyRemovalCharge","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setPerformanceCoefficients","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"attestationsWeight","type":"uint256","internalType":"uint256"},{"name":"blocksWeight","type":"uint256","internalType":"uint256"},{"name":"syncWeight","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setPerformanceLeewayData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"keyPivots","type":"uint256[]","internalType":"uint256[]"},{"name":"performanceLeeways","type":"uint256[]","internalType":"uint256[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setPriorityQueueLimit","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setRewardShareData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"keyPivots","type":"uint256[]","internalType":"uint256[]"},{"name":"rewardShares","type":"uint256[]","internalType":"uint256[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setStrikesParams","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"lifetime","type":"uint256","internalType":"uint256"},{"name":"threshold","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"unsetBadPerformancePenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetElRewardsStealingAdditionalFine","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetKeyRemovalCharge","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetPerformanceCoefficients","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetPerformanceLeewayData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetPriorityQueueLimit","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetRewardShareData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetStrikesParams","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"BadPerformancePenaltySet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"penalty","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BadPerformancePenaltyUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultBadPerformancePenaltySet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultElRewardsStealingAdditionalFineSet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultKeyRemovalChargeSet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultPerformanceCoefficientsSet","inputs":[{"name":"attestationsWeight","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"blocksWeight","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"syncWeight","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultPerformanceLeewaySet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultPriorityQueueLimitSet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultRewardShareSet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultStrikesParamsSet","inputs":[{"name":"lifetime","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"threshold","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ElRewardsStealingAdditionalFineSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"fine","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ElRewardsStealingAdditionalFineUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"KeyRemovalChargeSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"keyRemovalCharge","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"KeyRemovalChargeUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PerformanceCoefficientsSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"attestationsWeight","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"blocksWeight","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"syncWeight","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PerformanceCoefficientsUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PerformanceLeewayDataSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PerformanceLeewayDataUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PriorityQueueLimitSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"limit","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PriorityQueueLimitUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"RewardShareDataSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"RewardShareDataUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"StrikesParamsSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"lifetime","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"threshold","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"StrikesParamsUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidPerformanceLeewayData","inputs":[]},{"type":"error","name":"InvalidRewardShareData","inputs":[]},{"type":"error","name":"InvalidStrikesParams","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"SafeCastOverflowedUintDowncast","inputs":[{"name":"bits","type":"uint8","internalType":"uint8"},{"name":"value","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"ZeroAdminAddress","inputs":[]}] \ No newline at end of file diff --git a/src/providers/execution/contracts/cs_accounting.py b/src/providers/execution/contracts/cs_accounting.py index 786ea7f27..8a1000be3 100644 --- a/src/providers/execution/contracts/cs_accounting.py +++ b/src/providers/execution/contracts/cs_accounting.py @@ -4,6 +4,7 @@ from web3 import Web3 from web3.types import BlockIdentifier +from src.types import NodeOperatorId from ..base_interface import ContractInterface logger = logging.getLogger(__name__) @@ -24,3 +25,16 @@ def fee_distributor(self, block_identifier: BlockIdentifier = "latest") -> Check } ) return Web3.to_checksum_address(resp) + + def get_bond_curve_id(self, node_operator_id: NodeOperatorId, block_identifier: BlockIdentifier = "latest") -> int: + """Returns the curve ID""" + + resp = self.functions.getBondCurveId(node_operator_id).call(block_identifier=block_identifier) + logger.info( + { + "msg": f"Call `getBondCurveId({node_operator_id})`.", + "value": resp, + "block_identifier": repr(block_identifier), + } + ) + return resp diff --git a/src/providers/execution/contracts/cs_module.py b/src/providers/execution/contracts/cs_module.py index 2b0b37e29..3b1558a5e 100644 --- a/src/providers/execution/contracts/cs_module.py +++ b/src/providers/execution/contracts/cs_module.py @@ -43,6 +43,19 @@ def accounting(self, block_identifier: BlockIdentifier = "latest") -> ChecksumAd ) return Web3.to_checksum_address(resp) + def parameters_registry(self, block_identifier: BlockIdentifier = "latest") -> ChecksumAddress: + """Returns the address of the CSParametersRegistry contract""" + + resp = self.functions.PARAMETERS_REGISTRY().call(block_identifier=block_identifier) + logger.info( + { + "msg": "Call `PARAMETERS_REGISTRY()`.", + "value": resp, + "block_identifier": repr(block_identifier), + } + ) + return Web3.to_checksum_address(resp) + def is_paused(self, block: BlockIdentifier = "latest") -> bool: resp = self.functions.isPaused().call(block_identifier=block) logger.info( diff --git a/src/providers/execution/contracts/cs_parameters_registry.py b/src/providers/execution/contracts/cs_parameters_registry.py new file mode 100644 index 000000000..f0f0e1946 --- /dev/null +++ b/src/providers/execution/contracts/cs_parameters_registry.py @@ -0,0 +1,114 @@ +import logging +from dataclasses import dataclass + +from web3.types import BlockIdentifier + +from src.constants import UINT256_MAX, TOTAL_BASIS_POINTS +from src.providers.execution.base_interface import ContractInterface + +logger = logging.getLogger(__name__) + + +@dataclass +class PerformanceCoefficients: + attestations_weight: int = 54 + blocks_weight: int = 8 + sync_weight: int = 2 + + def calc_performance(self, att_aggr, prop_aggr, sync_aggr) -> float: + base = self.attestations_weight + performance = att_aggr.perf * self.attestations_weight + + if prop_aggr: + base += self.blocks_weight + performance += prop_aggr.perf * self.blocks_weight + + if sync_aggr: + base += self.sync_weight + performance += sync_aggr.perf * self.sync_weight + + performance /= base + + if performance > 1: + raise ValueError(f"Invalid performance: {performance=}") + + return performance + + +@dataclass +class RewardShare: + key_pivots: list[int] + reward_shares: list[int] + + def get_for(self, key_number: int) -> float: + for i, pivot_number in enumerate(self.key_pivots + [UINT256_MAX]): + if key_number <= pivot_number: + return self.reward_shares[i] / TOTAL_BASIS_POINTS + raise ValueError(f"Key number {key_number} is out of {self.key_pivots}") + + +@dataclass +class PerformanceLeeway: + key_pivots: list[int] + performance_leeways: list[int] + + def get_for(self, key_number: int) -> float: + for i, pivot_number in enumerate(self.key_pivots + [UINT256_MAX]): + if key_number <= pivot_number: + return self.performance_leeways[i] / TOTAL_BASIS_POINTS + raise ValueError(f"Key number {key_number} is out of {self.key_pivots}") + + +class CSParametersRegistryContract(ContractInterface): + abi_path = "./assets/CSParametersRegistry.json" + + def get_performance_coefficients( + self, + curve_id: int, + block_identifier: BlockIdentifier = "latest" + ) -> PerformanceCoefficients: + """Returns performance coefficients for given node operator""" + + resp = self.functions.getPerformanceCoefficients(curve_id).call(block_identifier=block_identifier) + logger.info( + { + "msg": f"Call `getPerformanceCoefficients({curve_id})`.", + "value": resp, + "block_identifier": repr(block_identifier), + } + ) + return PerformanceCoefficients(*resp) + + def get_reward_share_data( + self, + curve_id: int, + block_identifier: BlockIdentifier = "latest" + ) -> RewardShare: + """Returns reward share data for given node operator""" + + resp = self.functions.getRewardShareData(curve_id).call(block_identifier=block_identifier) + logger.info( + { + "msg": f"Call `getRewardShareData({curve_id})`.", + "value": resp, + "block_identifier": repr(block_identifier), + } + ) + return RewardShare(*resp) + + def get_performance_leeway_data( + self, + curve_id: int, + block_identifier: BlockIdentifier = "latest" + ) -> PerformanceLeeway: + """Returns performance leeway data for given node operator""" + + resp = self.functions.getPerformanceLeewayData(curve_id).call(block_identifier=block_identifier) + logger.info( + { + "msg": f"Call `getPerformanceLeewayData({curve_id})`.", + "value": resp, + "block_identifier": repr(block_identifier), + } + ) + return PerformanceLeeway(*resp) \ No newline at end of file diff --git a/src/web3py/extensions/csm.py b/src/web3py/extensions/csm.py index 65206cda7..2f80a2b39 100644 --- a/src/web3py/extensions/csm.py +++ b/src/web3py/extensions/csm.py @@ -19,6 +19,7 @@ from src.providers.execution.contracts.cs_fee_distributor import CSFeeDistributorContract from src.providers.execution.contracts.cs_fee_oracle import CSFeeOracleContract from src.providers.execution.contracts.cs_module import CSModuleContract +from src.providers.execution.contracts.cs_parameters_registry import CSParametersRegistryContract from src.providers.ipfs import CID, CIDv0, CIDv1, is_cid_v0 from src.types import BlockStamp, SlotNumber from src.utils.events import get_events_in_range @@ -31,8 +32,10 @@ class CSM(Module): w3: Web3 oracle: CSFeeOracleContract + accounting: CSAccountingContract fee_distributor: CSFeeDistributorContract module: CSModuleContract + params: CSParametersRegistryContract def __init__(self, w3: Web3) -> None: super().__init__(w3) @@ -88,7 +91,16 @@ def _load_contracts(self) -> None: ), ) - accounting = cast( + self.params = cast( + CSParametersRegistryContract, + self.w3.eth.contract( + address=self.module.parameters_registry(), + ContractFactoryClass=CSParametersRegistryContract, + decode_tuples=True, + ), + ) + + self.accounting = cast( CSAccountingContract, self.w3.eth.contract( address=self.module.accounting(), @@ -100,7 +112,7 @@ def _load_contracts(self) -> None: self.fee_distributor = cast( CSFeeDistributorContract, self.w3.eth.contract( - address=accounting.fee_distributor(), + address=self.accounting.fee_distributor(), ContractFactoryClass=CSFeeDistributorContract, decode_tuples=True, ), From 4c15f19d19ed5cba4ea136d9253e84a86aa9a46d Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Fri, 21 Feb 2025 09:55:41 +0100 Subject: [PATCH 030/162] fix: mypy --- src/constants.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/constants.py b/src/constants.py index ec7495d27..f1cea0eed 100644 --- a/src/constants.py +++ b/src/constants.py @@ -44,3 +44,4 @@ GWEI_TO_WEI = 10**9 MAX_BLOCK_GAS_LIMIT = 30_000_000 UINT64_MAX = 2**64 - 1 +UINT256_MAX = 2**256 - 1 From 3f152939be601d09aa21bb2ad973c16a83a100f5 Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Wed, 19 Feb 2025 16:27:34 +0100 Subject: [PATCH 031/162] refactor: split Tree into an ABC and RewardTree --- poetry.lock | 74 +++++++++++++++------------- pyproject.toml | 2 +- src/modules/csm/csm.py | 10 ++-- src/modules/csm/tree.py | 18 +++++-- src/modules/csm/types.py | 2 +- tests/modules/csm/test_csm_module.py | 14 +++--- tests/modules/csm/test_tree.py | 22 ++++++--- 7 files changed, 82 insertions(+), 60 deletions(-) diff --git a/poetry.lock b/poetry.lock index e4c1706a8..820344c5d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. [[package]] name = "aiohttp" @@ -1567,46 +1567,52 @@ files = [ [[package]] name = "mypy" -version = "1.10.0" +version = "1.15.0" description = "Optional static typing for Python" optional = false -python-versions = ">=3.8" -files = [ - {file = "mypy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2"}, - {file = "mypy-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99"}, - {file = "mypy-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2"}, - {file = "mypy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9"}, - {file = "mypy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051"}, - {file = "mypy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1"}, - {file = "mypy-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee"}, - {file = "mypy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de"}, - {file = "mypy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7"}, - {file = "mypy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53"}, - {file = "mypy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b"}, - {file = "mypy-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30"}, - {file = "mypy-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e"}, - {file = "mypy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5"}, - {file = "mypy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda"}, - {file = "mypy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0"}, - {file = "mypy-1.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727"}, - {file = "mypy-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4"}, - {file = "mypy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061"}, - {file = "mypy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f"}, - {file = "mypy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976"}, - {file = "mypy-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec"}, - {file = "mypy-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821"}, - {file = "mypy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746"}, - {file = "mypy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a"}, - {file = "mypy-1.10.0-py3-none-any.whl", hash = "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee"}, - {file = "mypy-1.10.0.tar.gz", hash = "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131"}, +python-versions = ">=3.9" +files = [ + {file = "mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13"}, + {file = "mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559"}, + {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b"}, + {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3"}, + {file = "mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b"}, + {file = "mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828"}, + {file = "mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f"}, + {file = "mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5"}, + {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e"}, + {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c"}, + {file = "mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f"}, + {file = "mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f"}, + {file = "mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd"}, + {file = "mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f"}, + {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464"}, + {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee"}, + {file = "mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e"}, + {file = "mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22"}, + {file = "mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445"}, + {file = "mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d"}, + {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5"}, + {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036"}, + {file = "mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357"}, + {file = "mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf"}, + {file = "mypy-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e601a7fa172c2131bff456bb3ee08a88360760d0d2f8cbd7a75a65497e2df078"}, + {file = "mypy-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:712e962a6357634fef20412699a3655c610110e01cdaa6180acec7fc9f8513ba"}, + {file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95579473af29ab73a10bada2f9722856792a36ec5af5399b653aa28360290a5"}, + {file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f8722560a14cde92fdb1e31597760dc35f9f5524cce17836c0d22841830fd5b"}, + {file = "mypy-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fbb8da62dc352133d7d7ca90ed2fb0e9d42bb1a32724c287d3c76c58cbaa9c2"}, + {file = "mypy-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:d10d994b41fb3497719bbf866f227b3489048ea4bbbb5015357db306249f7980"}, + {file = "mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e"}, + {file = "mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43"}, ] [package.dependencies] -mypy-extensions = ">=1.0.0" -typing-extensions = ">=4.1.0" +mypy_extensions = ">=1.0.0" +typing_extensions = ">=4.6.0" [package.extras] dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] install-types = ["pip"] mypyc = ["setuptools (>=50)"] reports = ["lxml"] @@ -2702,4 +2708,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "d7a100a7dfd7d422c0ab83125c79a2b8c0a7aa107a4008601d78002325abcdcf" +content-hash = "28553c7659d77641c89b5e36835efa957a360e947306ca8072272c953ea748fb" diff --git a/pyproject.toml b/pyproject.toml index 2db66c692..030dff73a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ types-urllib3 = "^1.26.25.8" hypothesis = "^6.68.2" black = "^24.8" pylint = "^3.2.3" -mypy = "^1.10.0" +mypy = "^1.12.0" requests-mock = "^1.12.1" [build-system] diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index e9e042172..5fd154adc 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -14,7 +14,7 @@ from src.modules.csm.checkpoint import FrameCheckpointProcessor, FrameCheckpointsIterator, MinStepIsNotReached from src.modules.csm.log import FramePerfLog, OperatorFrameSummary from src.modules.csm.state import State, Frame, AttestationsAccumulator -from src.modules.csm.tree import Tree +from src.modules.csm.tree import RewardTree from src.modules.csm.types import ReportData, Shares from src.modules.submodules.consensus import ConsensusModule from src.modules.submodules.oracle_module import BaseModule, ModuleExecuteDelay @@ -358,7 +358,7 @@ def calc_rewards_distribution_in_frame( def get_accumulated_rewards(self, cid: CID, root: HexBytes) -> Iterator[tuple[NodeOperatorId, Shares]]: logger.info({"msg": "Fetching tree by CID from IPFS", "cid": repr(cid)}) - tree = Tree.decode(self.w3.ipfs.fetch(cid)) + tree = RewardTree.decode(self.w3.ipfs.fetch(cid)) logger.info({"msg": "Restored tree from IPFS dump", "root": repr(tree.root)}) @@ -393,7 +393,7 @@ def get_stuck_operators(self, frame: Frame, frame_blockstamp: ReferenceBlockStam ) return set(stuck_from_digests) | set(stuck_from_events) - def make_tree(self, shares: dict[NodeOperatorId, Shares]) -> Tree: + def make_tree(self, shares: dict[NodeOperatorId, Shares]) -> RewardTree: if not shares: raise ValueError("No shares to build a tree") @@ -407,11 +407,11 @@ def make_tree(self, shares: dict[NodeOperatorId, Shares]) -> Tree: if stone in shares and len(shares) > 2: shares.pop(stone) - tree = Tree.new(tuple((no_id, amount) for (no_id, amount) in shares.items())) + tree = RewardTree.new(tuple((no_id, amount) for (no_id, amount) in shares.items())) logger.info({"msg": "New tree built for the report", "root": repr(tree.root)}) return tree - def publish_tree(self, tree: Tree) -> CID: + def publish_tree(self, tree: RewardTree) -> CID: tree_cid = self.w3.ipfs.publish(tree.encode()) logger.info({"msg": "Tree dump uploaded to IPFS", "cid": repr(tree_cid)}) return tree_cid diff --git a/src/modules/csm/tree.py b/src/modules/csm/tree.py index e38c0e36a..077435229 100644 --- a/src/modules/csm/tree.py +++ b/src/modules/csm/tree.py @@ -1,6 +1,7 @@ import json +from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import Self, Sequence +from typing import Iterable, Self, Sequence from hexbytes import HexBytes from oz_merkle_tree import Dump, StandardMerkleTree @@ -19,10 +20,10 @@ def default(self, o): @dataclass -class Tree: +class Tree[LeafType: Iterable](ABC): """A wrapper around StandardMerkleTree to cover use cases of the CSM oracle""" - tree: StandardMerkleTree[RewardTreeLeaf] + tree: StandardMerkleTree[LeafType] @property def root(self) -> HexBytes: @@ -50,10 +51,17 @@ def encode(self) -> bytes: .encode() ) - def dump(self) -> Dump[RewardTreeLeaf]: + def dump(self) -> Dump[LeafType]: return self.tree.dump() @classmethod - def new(cls, values: Sequence[RewardTreeLeaf]) -> Self: + @abstractmethod + def new(cls, values: Sequence[LeafType]) -> Self: + raise NotImplementedError + + +class RewardTree(Tree[RewardTreeLeaf]): + @classmethod + def new(cls, values) -> Self: """Create new instance around the wrapped tree out of the given values""" return cls(StandardMerkleTree(values, ("uint256", "uint256"))) diff --git a/src/modules/csm/types.py b/src/modules/csm/types.py index 541a0fc5f..ac0619cb5 100644 --- a/src/modules/csm/types.py +++ b/src/modules/csm/types.py @@ -11,7 +11,7 @@ Shares: TypeAlias = int -RewardTreeLeaf: TypeAlias = tuple[NodeOperatorId, Shares] +type RewardTreeLeaf = tuple[NodeOperatorId, Shares] @dataclass diff --git a/tests/modules/csm/test_csm_module.py b/tests/modules/csm/test_csm_module.py index ee096b64d..757647c31 100644 --- a/tests/modules/csm/test_csm_module.py +++ b/tests/modules/csm/test_csm_module.py @@ -1,8 +1,8 @@ import logging from collections import defaultdict from dataclasses import dataclass -from typing import NoReturn, Iterable, Literal, Type -from unittest.mock import Mock, patch, PropertyMock +from typing import Iterable, Literal, NoReturn, Type +from unittest.mock import Mock, PropertyMock, patch import pytest from hexbytes import HexBytes @@ -10,11 +10,11 @@ from src.constants import UINT64_MAX from src.modules.csm.csm import CSOracle from src.modules.csm.state import State -from src.modules.csm.tree import Tree +from src.modules.csm.tree import RewardTree, Tree from src.modules.submodules.oracle_module import ModuleExecuteDelay -from src.modules.submodules.types import CurrentFrame, ZERO_HASH -from src.providers.ipfs import CIDv0, CID -from src.types import NodeOperatorId, SlotNumber, StakingModuleId +from src.modules.submodules.types import ZERO_HASH, CurrentFrame +from src.providers.ipfs import CID, CIDv0 +from src.types import NodeOperatorId, SlotNumber from src.web3py.extensions.csm import CSM from tests.factory.blockstamp import BlockStampFactory, ReferenceBlockStampFactory from tests.factory.configs import ChainConfigFactory, FrameConfigFactory @@ -623,7 +623,7 @@ def test_execute_module_processed(module: CSOracle): @pytest.fixture() def tree(): - return Tree.new( + return RewardTree.new( [ (NodeOperatorId(0), 0), (NodeOperatorId(1), 1), diff --git a/tests/modules/csm/test_tree.py b/tests/modules/csm/test_tree.py index 623b4056e..3c8865a1c 100644 --- a/tests/modules/csm/test_tree.py +++ b/tests/modules/csm/test_tree.py @@ -1,3 +1,5 @@ +from typing import Self + import pytest from src.constants import UINT64_MAX @@ -5,9 +7,15 @@ from src.types import NodeOperatorId +class SimpleTree(Tree[tuple[NodeOperatorId, int]]): + @classmethod + def new(cls, values) -> Self: + return cls(StandardMerkleTree(values, ("uint256", "uint256"))) + + @pytest.fixture() def tree(): - return Tree.new( + return SimpleTree.new( [ (NodeOperatorId(0), 0), (NodeOperatorId(1), 1), @@ -17,20 +25,20 @@ def tree(): ) -def test_non_null_root(tree: Tree): +def test_non_null_root(tree: SimpleTree): assert tree.root -def test_encode_decode(tree: Tree): - decoded = Tree.decode(tree.encode()) +def test_encode_decode(tree: SimpleTree): + decoded = SimpleTree.decode(tree.encode()) assert decoded.root == tree.root -def test_decode_plain_tree_dump(tree: Tree): - decoded = Tree.decode(TreeJSONEncoder().encode(tree.tree.dump()).encode()) +def test_decode_plain_tree_dump(tree: SimpleTree): + decoded = SimpleTree.decode(TreeJSONEncoder().encode(tree.tree.dump()).encode()) assert decoded.root == tree.root -def test_dump_compatibility(tree: Tree): +def test_dump_compatibility(tree: SimpleTree): loaded = StandardMerkleTree.load(tree.dump()) assert loaded.root == tree.root From 998a7c78d8f6b5fffb3815fa54db72bfc6914623 Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Thu, 20 Feb 2025 12:40:30 +0100 Subject: [PATCH 032/162] wip: add StrikeTree --- src/modules/csm/tree.py | 9 ++- src/modules/csm/types.py | 2 + tests/modules/csm/test_tree.py | 105 ++++++++++++++++++++++++++------- 3 files changed, 93 insertions(+), 23 deletions(-) diff --git a/src/modules/csm/tree.py b/src/modules/csm/tree.py index 077435229..8dad2ef7f 100644 --- a/src/modules/csm/tree.py +++ b/src/modules/csm/tree.py @@ -6,7 +6,7 @@ from hexbytes import HexBytes from oz_merkle_tree import Dump, StandardMerkleTree -from src.modules.csm.types import RewardTreeLeaf +from src.modules.csm.types import RewardTreeLeaf, StrikeTreeLeaf from src.providers.ipfs.cid import CID @@ -65,3 +65,10 @@ class RewardTree(Tree[RewardTreeLeaf]): def new(cls, values) -> Self: """Create new instance around the wrapped tree out of the given values""" return cls(StandardMerkleTree(values, ("uint256", "uint256"))) + + +class StrikeTree(Tree[StrikeTreeLeaf]): + @classmethod + def new(cls, values) -> Self: + """Create new instance around the wrapped tree out of the given values""" + return cls(StandardMerkleTree(values, ("uint256", "uint256[]"))) diff --git a/src/modules/csm/types.py b/src/modules/csm/types.py index ac0619cb5..9bcb4e7af 100644 --- a/src/modules/csm/types.py +++ b/src/modules/csm/types.py @@ -3,6 +3,7 @@ from typing import TypeAlias, Literal from hexbytes import HexBytes +from web3.types import Timestamp from src.providers.ipfs import CID from src.types import NodeOperatorId, SlotNumber @@ -12,6 +13,7 @@ Shares: TypeAlias = int type RewardTreeLeaf = tuple[NodeOperatorId, Shares] +type StrikeTreeLeaf = tuple[NodeOperatorId, list[Timestamp]] @dataclass diff --git a/tests/modules/csm/test_tree.py b/tests/modules/csm/test_tree.py index 3c8865a1c..3c4686a10 100644 --- a/tests/modules/csm/test_tree.py +++ b/tests/modules/csm/test_tree.py @@ -1,44 +1,105 @@ -from typing import Self +from abc import ABC, abstractmethod +from typing import Iterable, Self import pytest +from web3.types import Timestamp from src.constants import UINT64_MAX -from src.modules.csm.tree import StandardMerkleTree, Tree, TreeJSONEncoder +from src.modules.csm.tree import RewardTree, StandardMerkleTree, StrikeTree, Tree, TreeJSONEncoder +from src.modules.csm.types import RewardTreeLeaf, StrikeTreeLeaf from src.types import NodeOperatorId -class SimpleTree(Tree[tuple[NodeOperatorId, int]]): +class TreeTestBase[LeafType: Iterable](ABC): + type TreeType = Tree[LeafType] + + cls: type[Tree[LeafType]] + + @property + @abstractmethod + def values(self) -> list[LeafType]: + raise NotImplementedError + + @pytest.fixture() + def tree(self) -> TreeType: + return self.cls.new(self.values) + + def test_non_null_root(self, tree: TreeType): + assert tree.root + + def test_encode_decode(self, tree: TreeType): + decoded = self.cls.decode(tree.encode()) + assert decoded.root == tree.root + + decoded_values = [v["value"] for v in decoded.tree.values] + assert decoded_values == convert_tuples(self.values) + + def test_decode_plain_tree_dump(self, tree: TreeType): + decoded = self.cls.decode(TreeJSONEncoder().encode(tree.tree.dump()).encode()) + assert decoded.root == tree.root + + def test_dump_compatibility(self, tree: TreeType): + loaded = StandardMerkleTree.load(tree.dump()) + assert loaded.root == tree.root + + +type SimpleLeaf = tuple[int] + + +class SimpleTree(Tree[SimpleLeaf]): @classmethod def new(cls, values) -> Self: - return cls(StandardMerkleTree(values, ("uint256", "uint256"))) + return cls(StandardMerkleTree(values, ("uint256",))) + + +class TestSimpleTree(TreeTestBase[SimpleLeaf]): + cls = SimpleTree + + @property + def values(self): + return [ + (0,), + (1,), + (2,), + (UINT64_MAX,), + ] + +class TestRewardTree(TreeTestBase[RewardTreeLeaf]): + cls = RewardTree -@pytest.fixture() -def tree(): - return SimpleTree.new( - [ + @property + def values(self) -> list[RewardTreeLeaf]: + return [ (NodeOperatorId(0), 0), (NodeOperatorId(1), 1), (NodeOperatorId(2), 42), (NodeOperatorId(UINT64_MAX), 0), ] - ) - -def test_non_null_root(tree: SimpleTree): - assert tree.root +class TestStrikeTree(TreeTestBase[StrikeTreeLeaf]): + cls = StrikeTree -def test_encode_decode(tree: SimpleTree): - decoded = SimpleTree.decode(tree.encode()) - assert decoded.root == tree.root - + @property + def values(self) -> list[StrikeTreeLeaf]: + return [ + (NodeOperatorId(0), [Timestamp(0)]), + (NodeOperatorId(1), [Timestamp(1), Timestamp(2), Timestamp(3)]), + (NodeOperatorId(2), [Timestamp(42)]), + (NodeOperatorId(UINT64_MAX), [Timestamp(1), Timestamp(2), Timestamp(3), Timestamp(4)]), + ] -def test_decode_plain_tree_dump(tree: SimpleTree): - decoded = SimpleTree.decode(TreeJSONEncoder().encode(tree.tree.dump()).encode()) - assert decoded.root == tree.root +def convert_tuples(obj: Iterable): + """ + A helper that converts all tuples in an iterable to lists. JSON has no notion of a tuple, so in + order to compare values with those decoded from JSON, a conversion is required. + """ -def test_dump_compatibility(tree: SimpleTree): - loaded = StandardMerkleTree.load(tree.dump()) - assert loaded.root == tree.root + if isinstance(obj, tuple): + return [convert_tuples(item) for item in obj] + elif isinstance(obj, list): + return [convert_tuples(item) for item in obj] + else: + return obj From 9135457cbddbef5f4ed548640f7e883ed5540747 Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Thu, 20 Feb 2025 13:27:47 +0100 Subject: [PATCH 033/162] feat: custom encoder/decoder for tree class --- src/modules/csm/tree.py | 37 ++++++++++++++++++++++++++++------ src/modules/csm/types.py | 3 ++- tests/modules/csm/test_tree.py | 11 ++++++---- 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/src/modules/csm/tree.py b/src/modules/csm/tree.py index 8dad2ef7f..b395bfef1 100644 --- a/src/modules/csm/tree.py +++ b/src/modules/csm/tree.py @@ -1,16 +1,18 @@ import json from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import Iterable, Self, Sequence +from json import JSONDecodeError, JSONDecoder, JSONEncoder +from typing import Any, Iterable, Self, Sequence from hexbytes import HexBytes from oz_merkle_tree import Dump, StandardMerkleTree from src.modules.csm.types import RewardTreeLeaf, StrikeTreeLeaf from src.providers.ipfs.cid import CID +from src.utils.types import hex_str_to_bytes -class TreeJSONEncoder(json.JSONEncoder): +class TreeJSONEncoder(JSONEncoder): def default(self, o): if isinstance(o, bytes): return f"0x{o.hex()}" @@ -25,6 +27,9 @@ class Tree[LeafType: Iterable](ABC): tree: StandardMerkleTree[LeafType] + encoder: type[JSONEncoder] = TreeJSONEncoder + decoder: type[JSONDecoder] = JSONDecoder + @property def root(self) -> HexBytes: return HexBytes(self.tree.root) @@ -34,15 +39,15 @@ def decode(cls, content: bytes) -> Self: """Restore a tree from a supported binary representation""" try: - return cls(StandardMerkleTree.load(json.loads(content))) - except json.JSONDecodeError as e: + return cls(StandardMerkleTree.load(json.loads(content, cls=cls.decoder))) + except JSONDecodeError as e: raise ValueError("Unsupported tree format") from e def encode(self) -> bytes: """Convert the underlying StandardMerkleTree to a binary representation""" return ( - TreeJSONEncoder( + self.encoder( indent=None, separators=(',', ':'), sort_keys=True, @@ -67,8 +72,28 @@ def new(cls, values) -> Self: return cls(StandardMerkleTree(values, ("uint256", "uint256"))) +class StrikeTreeJSONDecoder(JSONDecoder): + # NOTE: object_pairs_hook is set unconditionally upon object initialisation, so it's required to + # override the __init__ method. + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs, object_pairs_hook=self.__object_pairs_hook) + + @staticmethod + def __object_pairs_hook(items: list[tuple[str, Any]]): + def try_convert_all_hex_str_to_bytes(obj: Any): + if isinstance(obj, list): + return [try_convert_all_hex_str_to_bytes(item) for item in obj] + if isinstance(obj, str) and obj.startswith("0x"): + return hex_str_to_bytes(obj) + return obj + + return {key: try_convert_all_hex_str_to_bytes(value) for key, value in items} + + class StrikeTree(Tree[StrikeTreeLeaf]): + decoder = StrikeTreeJSONDecoder + @classmethod def new(cls, values) -> Self: """Create new instance around the wrapped tree out of the given values""" - return cls(StandardMerkleTree(values, ("uint256", "uint256[]"))) + return cls(StandardMerkleTree(values, ("uint256", "bytes", "uint256[]"))) diff --git a/src/modules/csm/types.py b/src/modules/csm/types.py index 9bcb4e7af..303f68b8b 100644 --- a/src/modules/csm/types.py +++ b/src/modules/csm/types.py @@ -2,6 +2,7 @@ from dataclasses import dataclass from typing import TypeAlias, Literal +from eth_typing import HexStr from hexbytes import HexBytes from web3.types import Timestamp @@ -13,7 +14,7 @@ Shares: TypeAlias = int type RewardTreeLeaf = tuple[NodeOperatorId, Shares] -type StrikeTreeLeaf = tuple[NodeOperatorId, list[Timestamp]] +type StrikeTreeLeaf = tuple[NodeOperatorId, bytes, list[Timestamp]] @dataclass diff --git a/tests/modules/csm/test_tree.py b/tests/modules/csm/test_tree.py index 3c4686a10..c380d3d94 100644 --- a/tests/modules/csm/test_tree.py +++ b/tests/modules/csm/test_tree.py @@ -2,7 +2,9 @@ from typing import Iterable, Self import pytest +from eth_typing import HexStr from web3.types import Timestamp +from src.utils.types import hex_str_to_bytes from src.constants import UINT64_MAX from src.modules.csm.tree import RewardTree, StandardMerkleTree, StrikeTree, Tree, TreeJSONEncoder @@ -84,10 +86,11 @@ class TestStrikeTree(TreeTestBase[StrikeTreeLeaf]): @property def values(self) -> list[StrikeTreeLeaf]: return [ - (NodeOperatorId(0), [Timestamp(0)]), - (NodeOperatorId(1), [Timestamp(1), Timestamp(2), Timestamp(3)]), - (NodeOperatorId(2), [Timestamp(42)]), - (NodeOperatorId(UINT64_MAX), [Timestamp(1), Timestamp(2), Timestamp(3), Timestamp(4)]), + (NodeOperatorId(0), hex_str_to_bytes("0x00"), [Timestamp(0)]), + (NodeOperatorId(1), hex_str_to_bytes("0x01"), [Timestamp(1)]), + (NodeOperatorId(1), hex_str_to_bytes("0x02"), [Timestamp(1)]), + (NodeOperatorId(2), hex_str_to_bytes("0x03"), [Timestamp(42)]), + (NodeOperatorId(UINT64_MAX), hex_str_to_bytes("0x64"), [Timestamp(1), Timestamp(2), Timestamp(3)]), ] From 23fec9045d1a08a5cc5c8e67643788b655cf8dbf Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Thu, 20 Feb 2025 17:32:47 +0100 Subject: [PATCH 034/162] refactor: Tree subclasses --- src/modules/csm/csm.py | 10 +++++----- src/modules/csm/tree.py | 6 +++--- src/modules/csm/types.py | 4 ++-- tests/modules/csm/test_csm_module.py | 4 ++-- tests/modules/csm/test_tree.py | 19 +++++++++---------- 5 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index 5fd154adc..be6f22b04 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -14,7 +14,7 @@ from src.modules.csm.checkpoint import FrameCheckpointProcessor, FrameCheckpointsIterator, MinStepIsNotReached from src.modules.csm.log import FramePerfLog, OperatorFrameSummary from src.modules.csm.state import State, Frame, AttestationsAccumulator -from src.modules.csm.tree import RewardTree +from src.modules.csm.tree import RewardsTree, Tree from src.modules.csm.types import ReportData, Shares from src.modules.submodules.consensus import ConsensusModule from src.modules.submodules.oracle_module import BaseModule, ModuleExecuteDelay @@ -358,7 +358,7 @@ def calc_rewards_distribution_in_frame( def get_accumulated_rewards(self, cid: CID, root: HexBytes) -> Iterator[tuple[NodeOperatorId, Shares]]: logger.info({"msg": "Fetching tree by CID from IPFS", "cid": repr(cid)}) - tree = RewardTree.decode(self.w3.ipfs.fetch(cid)) + tree = RewardsTree.decode(self.w3.ipfs.fetch(cid)) logger.info({"msg": "Restored tree from IPFS dump", "root": repr(tree.root)}) @@ -393,7 +393,7 @@ def get_stuck_operators(self, frame: Frame, frame_blockstamp: ReferenceBlockStam ) return set(stuck_from_digests) | set(stuck_from_events) - def make_tree(self, shares: dict[NodeOperatorId, Shares]) -> RewardTree: + def make_tree(self, shares: dict[NodeOperatorId, Shares]) -> RewardsTree: if not shares: raise ValueError("No shares to build a tree") @@ -407,11 +407,11 @@ def make_tree(self, shares: dict[NodeOperatorId, Shares]) -> RewardTree: if stone in shares and len(shares) > 2: shares.pop(stone) - tree = RewardTree.new(tuple((no_id, amount) for (no_id, amount) in shares.items())) + tree = RewardsTree.new(tuple((no_id, amount) for (no_id, amount) in shares.items())) logger.info({"msg": "New tree built for the report", "root": repr(tree.root)}) return tree - def publish_tree(self, tree: RewardTree) -> CID: + def publish_tree(self, tree: Tree) -> CID: tree_cid = self.w3.ipfs.publish(tree.encode()) logger.info({"msg": "Tree dump uploaded to IPFS", "cid": repr(tree_cid)}) return tree_cid diff --git a/src/modules/csm/tree.py b/src/modules/csm/tree.py index b395bfef1..3a7f9e613 100644 --- a/src/modules/csm/tree.py +++ b/src/modules/csm/tree.py @@ -7,7 +7,7 @@ from hexbytes import HexBytes from oz_merkle_tree import Dump, StandardMerkleTree -from src.modules.csm.types import RewardTreeLeaf, StrikeTreeLeaf +from src.modules.csm.types import RewardsTreeLeaf, StrikesTreeLeaf from src.providers.ipfs.cid import CID from src.utils.types import hex_str_to_bytes @@ -65,7 +65,7 @@ def new(cls, values: Sequence[LeafType]) -> Self: raise NotImplementedError -class RewardTree(Tree[RewardTreeLeaf]): +class RewardsTree(Tree[RewardsTreeLeaf]): @classmethod def new(cls, values) -> Self: """Create new instance around the wrapped tree out of the given values""" @@ -90,7 +90,7 @@ def try_convert_all_hex_str_to_bytes(obj: Any): return {key: try_convert_all_hex_str_to_bytes(value) for key, value in items} -class StrikeTree(Tree[StrikeTreeLeaf]): +class StrikesTree(Tree[StrikesTreeLeaf]): decoder = StrikeTreeJSONDecoder @classmethod diff --git a/src/modules/csm/types.py b/src/modules/csm/types.py index 303f68b8b..a1bda8dcd 100644 --- a/src/modules/csm/types.py +++ b/src/modules/csm/types.py @@ -13,8 +13,8 @@ Shares: TypeAlias = int -type RewardTreeLeaf = tuple[NodeOperatorId, Shares] -type StrikeTreeLeaf = tuple[NodeOperatorId, bytes, list[Timestamp]] +type RewardsTreeLeaf = tuple[NodeOperatorId, Shares] +type StrikesTreeLeaf = tuple[NodeOperatorId, bytes, list[Timestamp]] @dataclass diff --git a/tests/modules/csm/test_csm_module.py b/tests/modules/csm/test_csm_module.py index 757647c31..4dcf5fa9b 100644 --- a/tests/modules/csm/test_csm_module.py +++ b/tests/modules/csm/test_csm_module.py @@ -10,7 +10,7 @@ from src.constants import UINT64_MAX from src.modules.csm.csm import CSOracle from src.modules.csm.state import State -from src.modules.csm.tree import RewardTree, Tree +from src.modules.csm.tree import RewardsTree, Tree from src.modules.submodules.oracle_module import ModuleExecuteDelay from src.modules.submodules.types import ZERO_HASH, CurrentFrame from src.providers.ipfs import CID, CIDv0 @@ -623,7 +623,7 @@ def test_execute_module_processed(module: CSOracle): @pytest.fixture() def tree(): - return RewardTree.new( + return RewardsTree.new( [ (NodeOperatorId(0), 0), (NodeOperatorId(1), 1), diff --git a/tests/modules/csm/test_tree.py b/tests/modules/csm/test_tree.py index c380d3d94..2bd57f11c 100644 --- a/tests/modules/csm/test_tree.py +++ b/tests/modules/csm/test_tree.py @@ -2,14 +2,13 @@ from typing import Iterable, Self import pytest -from eth_typing import HexStr from web3.types import Timestamp -from src.utils.types import hex_str_to_bytes from src.constants import UINT64_MAX -from src.modules.csm.tree import RewardTree, StandardMerkleTree, StrikeTree, Tree, TreeJSONEncoder -from src.modules.csm.types import RewardTreeLeaf, StrikeTreeLeaf +from src.modules.csm.tree import RewardsTree, StandardMerkleTree, StrikesTree, Tree, TreeJSONEncoder +from src.modules.csm.types import RewardsTreeLeaf, StrikesTreeLeaf from src.types import NodeOperatorId +from src.utils.types import hex_str_to_bytes class TreeTestBase[LeafType: Iterable](ABC): @@ -67,11 +66,11 @@ def values(self): ] -class TestRewardTree(TreeTestBase[RewardTreeLeaf]): - cls = RewardTree +class TestRewardsTree(TreeTestBase[RewardsTreeLeaf]): + cls = RewardsTree @property - def values(self) -> list[RewardTreeLeaf]: + def values(self) -> list[RewardsTreeLeaf]: return [ (NodeOperatorId(0), 0), (NodeOperatorId(1), 1), @@ -80,11 +79,11 @@ def values(self) -> list[RewardTreeLeaf]: ] -class TestStrikeTree(TreeTestBase[StrikeTreeLeaf]): - cls = StrikeTree +class TestStrikesTree(TreeTestBase[StrikesTreeLeaf]): + cls = StrikesTree @property - def values(self) -> list[StrikeTreeLeaf]: + def values(self) -> list[StrikesTreeLeaf]: return [ (NodeOperatorId(0), hex_str_to_bytes("0x00"), [Timestamp(0)]), (NodeOperatorId(1), hex_str_to_bytes("0x01"), [Timestamp(1)]), From 6055b9da07eabaffa658222c4d542a55fcc6eb92 Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Thu, 20 Feb 2025 19:09:35 +0100 Subject: [PATCH 035/162] wip: StrikesList impl --- src/modules/csm/tree.py | 4 ++- src/modules/csm/types.py | 41 ++++++++++++++++++++--- tests/modules/csm/test_strikes.py | 36 +++++++++++++++++++++ tests/modules/csm/test_tree.py | 54 ++++++------------------------- 4 files changed, 85 insertions(+), 50 deletions(-) create mode 100644 tests/modules/csm/test_strikes.py diff --git a/src/modules/csm/tree.py b/src/modules/csm/tree.py index 3a7f9e613..afadfd17e 100644 --- a/src/modules/csm/tree.py +++ b/src/modules/csm/tree.py @@ -7,13 +7,15 @@ from hexbytes import HexBytes from oz_merkle_tree import Dump, StandardMerkleTree -from src.modules.csm.types import RewardsTreeLeaf, StrikesTreeLeaf +from src.modules.csm.types import RewardsTreeLeaf, StrikesList, StrikesTreeLeaf from src.providers.ipfs.cid import CID from src.utils.types import hex_str_to_bytes class TreeJSONEncoder(JSONEncoder): def default(self, o): + if isinstance(o, StrikesList): + return list(o) if isinstance(o, bytes): return f"0x{o.hex()}" if isinstance(o, CID): diff --git a/src/modules/csm/types.py b/src/modules/csm/types.py index a1bda8dcd..de60dde49 100644 --- a/src/modules/csm/types.py +++ b/src/modules/csm/types.py @@ -1,10 +1,8 @@ import logging from dataclasses import dataclass -from typing import TypeAlias, Literal +from typing import Iterable, Iterator, Literal, Sequence, TypeAlias -from eth_typing import HexStr from hexbytes import HexBytes -from web3.types import Timestamp from src.providers.ipfs import CID from src.types import NodeOperatorId, SlotNumber @@ -12,9 +10,44 @@ logger = logging.getLogger(__name__) +class StrikesList(Sequence): + """Deque-like structure to store strikes""" + + sentinel: int + data: list + + def __init__(self, data: Iterable[int]) -> None: + self.data = list(data) + self.sentinel = 0 + + def __len__(self) -> int: + return len(self.data) + + def __getitem__(self, index: int | slice): + return self.data.__getitem__(index) + + def __iter__(self) -> Iterator[int]: + return iter(self.data) + + def __eq__(self, value: object, /) -> bool: + return self.data.__eq__(value) + + def __repr__(self) -> str: + return repr(self.data) + + def resize(self, maxlen: int) -> None: + """Update maximum length of the list""" + self.data = self.data[:maxlen] + [self.sentinel] * (maxlen - len(self.data)) + + def push(self, item: int) -> None: + """Push element at the beginning of the list discarding the last element""" + self.data.insert(0, item) + self.data.pop(-1) + + Shares: TypeAlias = int type RewardsTreeLeaf = tuple[NodeOperatorId, Shares] -type StrikesTreeLeaf = tuple[NodeOperatorId, bytes, list[Timestamp]] +type StrikesTreeLeaf = tuple[NodeOperatorId, bytes, StrikesList] @dataclass diff --git a/tests/modules/csm/test_strikes.py b/tests/modules/csm/test_strikes.py new file mode 100644 index 000000000..ec24dcfc5 --- /dev/null +++ b/tests/modules/csm/test_strikes.py @@ -0,0 +1,36 @@ +from eth_utils.types import is_list_like + +from src.modules.csm.types import StrikesList + + +def test_create_empty(): + strikes = StrikesList([]) + assert not len(strikes) + + +def test_create_not_empty(): + strikes = StrikesList([1, 2, 3]) + assert strikes == [1, 2, 3] + + +def test_create_resize_to_smaller(): + strikes = StrikesList([1, 2, 3]) + strikes.resize(2) + assert strikes == [1, 2] + + +def test_create_resize_to_larger(): + strikes = StrikesList([1, 2, 3]) + strikes.resize(5) + assert strikes == [1, 2, 3, 0, 0] + + +def test_add_element(): + strikes = StrikesList([1, 2, 3]) + strikes.push(4) + assert strikes == [4, 1, 2] + + +def test_is_list_like(): + strikes = StrikesList([1, 2, 3]) + assert is_list_like(strikes) diff --git a/tests/modules/csm/test_tree.py b/tests/modules/csm/test_tree.py index 2bd57f11c..d919e9a25 100644 --- a/tests/modules/csm/test_tree.py +++ b/tests/modules/csm/test_tree.py @@ -1,12 +1,11 @@ from abc import ABC, abstractmethod -from typing import Iterable, Self +from typing import Iterable import pytest -from web3.types import Timestamp from src.constants import UINT64_MAX from src.modules.csm.tree import RewardsTree, StandardMerkleTree, StrikesTree, Tree, TreeJSONEncoder -from src.modules.csm.types import RewardsTreeLeaf, StrikesTreeLeaf +from src.modules.csm.types import RewardsTreeLeaf, StrikesList, StrikesTreeLeaf from src.types import NodeOperatorId from src.utils.types import hex_str_to_bytes @@ -32,8 +31,9 @@ def test_encode_decode(self, tree: TreeType): decoded = self.cls.decode(tree.encode()) assert decoded.root == tree.root + json_encode = self.cls.encoder().encode decoded_values = [v["value"] for v in decoded.tree.values] - assert decoded_values == convert_tuples(self.values) + assert json_encode(decoded_values) == json_encode(self.values) def test_decode_plain_tree_dump(self, tree: TreeType): decoded = self.cls.decode(TreeJSONEncoder().encode(tree.tree.dump()).encode()) @@ -44,28 +44,6 @@ def test_dump_compatibility(self, tree: TreeType): assert loaded.root == tree.root -type SimpleLeaf = tuple[int] - - -class SimpleTree(Tree[SimpleLeaf]): - @classmethod - def new(cls, values) -> Self: - return cls(StandardMerkleTree(values, ("uint256",))) - - -class TestSimpleTree(TreeTestBase[SimpleLeaf]): - cls = SimpleTree - - @property - def values(self): - return [ - (0,), - (1,), - (2,), - (UINT64_MAX,), - ] - - class TestRewardsTree(TreeTestBase[RewardsTreeLeaf]): cls = RewardsTree @@ -85,23 +63,9 @@ class TestStrikesTree(TreeTestBase[StrikesTreeLeaf]): @property def values(self) -> list[StrikesTreeLeaf]: return [ - (NodeOperatorId(0), hex_str_to_bytes("0x00"), [Timestamp(0)]), - (NodeOperatorId(1), hex_str_to_bytes("0x01"), [Timestamp(1)]), - (NodeOperatorId(1), hex_str_to_bytes("0x02"), [Timestamp(1)]), - (NodeOperatorId(2), hex_str_to_bytes("0x03"), [Timestamp(42)]), - (NodeOperatorId(UINT64_MAX), hex_str_to_bytes("0x64"), [Timestamp(1), Timestamp(2), Timestamp(3)]), + (NodeOperatorId(0), hex_str_to_bytes("0x00"), StrikesList([0])), + (NodeOperatorId(1), hex_str_to_bytes("0x01"), StrikesList([1])), + (NodeOperatorId(1), hex_str_to_bytes("0x02"), StrikesList([1])), + (NodeOperatorId(2), hex_str_to_bytes("0x03"), StrikesList([1])), + (NodeOperatorId(UINT64_MAX), hex_str_to_bytes("0x64"), StrikesList([1, 0, 1])), ] - - -def convert_tuples(obj: Iterable): - """ - A helper that converts all tuples in an iterable to lists. JSON has no notion of a tuple, so in - order to compare values with those decoded from JSON, a conversion is required. - """ - - if isinstance(obj, tuple): - return [convert_tuples(item) for item in obj] - elif isinstance(obj, list): - return [convert_tuples(item) for item in obj] - else: - return obj From 84b9354229d754f330c38ce843f29aed9129fc2a Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Fri, 21 Feb 2025 11:17:41 +0100 Subject: [PATCH 036/162] chore: fix tree types inhereted fields --- src/modules/csm/tree.py | 59 ++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/src/modules/csm/tree.py b/src/modules/csm/tree.py index afadfd17e..5aa099fb8 100644 --- a/src/modules/csm/tree.py +++ b/src/modules/csm/tree.py @@ -1,36 +1,52 @@ import json from abc import ABC, abstractmethod -from dataclasses import dataclass from json import JSONDecodeError, JSONDecoder, JSONEncoder -from typing import Any, Iterable, Self, Sequence +from typing import Any, ClassVar, Iterable, Self, Sequence from hexbytes import HexBytes from oz_merkle_tree import Dump, StandardMerkleTree from src.modules.csm.types import RewardsTreeLeaf, StrikesList, StrikesTreeLeaf -from src.providers.ipfs.cid import CID from src.utils.types import hex_str_to_bytes class TreeJSONEncoder(JSONEncoder): def default(self, o): - if isinstance(o, StrikesList): - return list(o) if isinstance(o, bytes): return f"0x{o.hex()}" - if isinstance(o, CID): - return str(o) return super().default(o) -@dataclass +class TreeJSONDecoder(JSONDecoder): + # NOTE: object_pairs_hook is set unconditionally upon object initialisation, so it's required to + # override the __init__ method. + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs, object_pairs_hook=self.__object_pairs_hook) + + @staticmethod + def __object_pairs_hook(items: list[tuple[str, Any]]): + def try_convert_all_hex_str_to_bytes(obj: Any): + if isinstance(obj, dict): + return {k: try_convert_all_hex_str_to_bytes(v) for (k, v) in obj.items()} + if isinstance(obj, list): + return [try_convert_all_hex_str_to_bytes(item) for item in obj] + if isinstance(obj, str) and obj.startswith("0x"): + return hex_str_to_bytes(obj) + return obj + + return {k: try_convert_all_hex_str_to_bytes(v) for k, v in items} + + class Tree[LeafType: Iterable](ABC): """A wrapper around StandardMerkleTree to cover use cases of the CSM oracle""" + encoder: ClassVar[type[JSONEncoder]] = TreeJSONEncoder + decoder: ClassVar[type[JSONDecoder]] = TreeJSONDecoder + tree: StandardMerkleTree[LeafType] - encoder: type[JSONEncoder] = TreeJSONEncoder - decoder: type[JSONDecoder] = JSONDecoder + def __init__(self, tree: StandardMerkleTree[LeafType]) -> None: + self.tree = tree @property def root(self) -> HexBytes: @@ -74,26 +90,15 @@ def new(cls, values) -> Self: return cls(StandardMerkleTree(values, ("uint256", "uint256"))) -class StrikeTreeJSONDecoder(JSONDecoder): - # NOTE: object_pairs_hook is set unconditionally upon object initialisation, so it's required to - # override the __init__ method. - def __init__(self, *args, **kwargs) -> None: - super().__init__(*args, **kwargs, object_pairs_hook=self.__object_pairs_hook) - - @staticmethod - def __object_pairs_hook(items: list[tuple[str, Any]]): - def try_convert_all_hex_str_to_bytes(obj: Any): - if isinstance(obj, list): - return [try_convert_all_hex_str_to_bytes(item) for item in obj] - if isinstance(obj, str) and obj.startswith("0x"): - return hex_str_to_bytes(obj) - return obj - - return {key: try_convert_all_hex_str_to_bytes(value) for key, value in items} +class StrikesTreeJSONEncoder(TreeJSONEncoder): + def default(self, o): + if isinstance(o, StrikesList): + return list(o) + return super().default(o) class StrikesTree(Tree[StrikesTreeLeaf]): - decoder = StrikeTreeJSONDecoder + encoder = StrikesTreeJSONEncoder @classmethod def new(cls, values) -> Self: From 572da36a834b5e39b8ada69b129bdb1c110d1841 Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Fri, 21 Feb 2025 11:18:31 +0100 Subject: [PATCH 037/162] chore: better typing for StrikesList --- src/modules/csm/types.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/modules/csm/types.py b/src/modules/csm/types.py index de60dde49..507dc31fa 100644 --- a/src/modules/csm/types.py +++ b/src/modules/csm/types.py @@ -1,6 +1,6 @@ import logging from dataclasses import dataclass -from typing import Iterable, Iterator, Literal, Sequence, TypeAlias +from typing import Iterable, Literal, Sequence, TypeAlias from hexbytes import HexBytes @@ -10,11 +10,11 @@ logger = logging.getLogger(__name__) -class StrikesList(Sequence): +class StrikesList(Sequence[int]): """Deque-like structure to store strikes""" sentinel: int - data: list + data: list[int] def __init__(self, data: Iterable[int]) -> None: self.data = list(data) @@ -23,11 +23,8 @@ def __init__(self, data: Iterable[int]) -> None: def __len__(self) -> int: return len(self.data) - def __getitem__(self, index: int | slice): - return self.data.__getitem__(index) - - def __iter__(self) -> Iterator[int]: - return iter(self.data) + def __getitem__(self, index): + return self.data[index] def __eq__(self, value: object, /) -> bool: return self.data.__eq__(value) From 631aa8324337c88670cddfa5f88196093df6a6c7 Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Fri, 21 Feb 2025 11:32:57 +0100 Subject: [PATCH 038/162] wip: add get_strikes_params --- .../contracts/cs_parameters_registry.py | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/src/providers/execution/contracts/cs_parameters_registry.py b/src/providers/execution/contracts/cs_parameters_registry.py index f0f0e1946..742019e7c 100644 --- a/src/providers/execution/contracts/cs_parameters_registry.py +++ b/src/providers/execution/contracts/cs_parameters_registry.py @@ -3,7 +3,7 @@ from web3.types import BlockIdentifier -from src.constants import UINT256_MAX, TOTAL_BASIS_POINTS +from src.constants import TOTAL_BASIS_POINTS, UINT256_MAX from src.providers.execution.base_interface import ContractInterface logger = logging.getLogger(__name__) @@ -59,13 +59,19 @@ def get_for(self, key_number: int) -> float: raise ValueError(f"Key number {key_number} is out of {self.key_pivots}") +@dataclass +class StrikesParams: + lifetime: int + threshold: int + + class CSParametersRegistryContract(ContractInterface): abi_path = "./assets/CSParametersRegistry.json" def get_performance_coefficients( self, curve_id: int, - block_identifier: BlockIdentifier = "latest" + block_identifier: BlockIdentifier = "latest", ) -> PerformanceCoefficients: """Returns performance coefficients for given node operator""" @@ -82,7 +88,7 @@ def get_performance_coefficients( def get_reward_share_data( self, curve_id: int, - block_identifier: BlockIdentifier = "latest" + block_identifier: BlockIdentifier = "latest", ) -> RewardShare: """Returns reward share data for given node operator""" @@ -99,7 +105,7 @@ def get_reward_share_data( def get_performance_leeway_data( self, curve_id: int, - block_identifier: BlockIdentifier = "latest" + block_identifier: BlockIdentifier = "latest", ) -> PerformanceLeeway: """Returns performance leeway data for given node operator""" @@ -111,4 +117,21 @@ def get_performance_leeway_data( "block_identifier": repr(block_identifier), } ) - return PerformanceLeeway(*resp) \ No newline at end of file + return PerformanceLeeway(*resp) + + def get_strikes_params( + self, + curve_id: int, + block_identifier: BlockIdentifier = "latest", + ) -> StrikesParams: + """Returns strikes params for a given curve id""" + + resp = self.functions.getStrikesParams(curve_id).call(block_identifier=block_identifier) + logger.info( + { + "msg": f"Call `getStrikesParams({curve_id})`.", + "value": resp, + "block_identifier": repr(block_identifier), + } + ) + return StrikesParams(*resp) From 049527bfe11cf8cb7b0b6c64cf4ee62178c95a8c Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Fri, 21 Feb 2025 11:33:29 +0100 Subject: [PATCH 039/162] chore: fix test_tree --- tests/modules/csm/test_tree.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/modules/csm/test_tree.py b/tests/modules/csm/test_tree.py index d919e9a25..470b6a99d 100644 --- a/tests/modules/csm/test_tree.py +++ b/tests/modules/csm/test_tree.py @@ -1,4 +1,5 @@ from abc import ABC, abstractmethod +from json import JSONDecoder, JSONEncoder from typing import Iterable import pytest @@ -15,6 +16,14 @@ class TreeTestBase[LeafType: Iterable](ABC): cls: type[Tree[LeafType]] + @property + def encoder(self) -> JSONEncoder: + return self.cls.encoder() + + @property + def decoder(self) -> JSONDecoder: + return self.cls.decoder() + @property @abstractmethod def values(self) -> list[LeafType]: @@ -31,12 +40,12 @@ def test_encode_decode(self, tree: TreeType): decoded = self.cls.decode(tree.encode()) assert decoded.root == tree.root - json_encode = self.cls.encoder().encode + json_encode = self.encoder.encode decoded_values = [v["value"] for v in decoded.tree.values] assert json_encode(decoded_values) == json_encode(self.values) def test_decode_plain_tree_dump(self, tree: TreeType): - decoded = self.cls.decode(TreeJSONEncoder().encode(tree.tree.dump()).encode()) + decoded = self.cls.decode(self.encoder.encode(tree.tree.dump()).encode()) assert decoded.root == tree.root def test_dump_compatibility(self, tree: TreeType): From 78b327e089280acbfba70838f974cadda4220f22 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Fri, 21 Feb 2025 19:20:26 +0100 Subject: [PATCH 040/162] feat: update to v2 in tests --- .github/workflows/mainnet_fork_tests.yml | 39 ++++++++-- .gitignore | 1 + tests/fork/conftest.py | 39 +++++++--- tests/fork/test_csm_oracle_cycle.py | 91 +++++++++++++++++++++++- tests/fork/utils/lock.py | 23 ++++++ 5 files changed, 176 insertions(+), 17 deletions(-) create mode 100644 tests/fork/utils/lock.py diff --git a/.github/workflows/mainnet_fork_tests.yml b/.github/workflows/mainnet_fork_tests.yml index 351ccdfba..7f7412a4e 100644 --- a/.github/workflows/mainnet_fork_tests.yml +++ b/.github/workflows/mainnet_fork_tests.yml @@ -21,10 +21,44 @@ permissions: jobs: tests: runs-on: ubuntu-latest - + env: + FORGE_REV: v0.3.0 steps: - uses: actions/checkout@v3 + # TODO: Remove after upgrade to CSM v2 on Mainnet. + - name: Checkout CSM repo + uses: actions/checkout@v4 + with: + repository: 'lidofinance/community-staking-module' + ref: 'develop' + path: 'testruns/community-staking-module' + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: ${{ env.FORGE_REV }} + + - name: Install node + uses: actions/setup-node@v4 + with: + node-version-file: "testruns/community-staking-module/.nvmrc" + cache: 'yarn' + cache-dependency-path: "testruns/community-staking-module/yarn.lock" + + - name: Install Just + uses: extractions/setup-just@v2 + with: + just-version: '1.24.0' + + - name: Install dependencies + working-directory: testruns/community-staking-module + run: just deps + + - name: Build contracts + working-directory: testruns/community-staking-module + run: just build + - name: Set up Python 3.12 uses: actions/setup-python@v4 with: @@ -42,9 +76,6 @@ jobs: run: | poetry install --no-interaction --with=dev - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 - - name: Mainnet Fork Tests run: poetry run pytest -m 'fork' -n auto tests env: diff --git a/.gitignore b/.gitignore index a12e4a1a0..e8866b716 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,7 @@ coverage.xml *.py,cover .hypothesis/ .pytest_cache/ +testruns/ # Translations *.mo diff --git a/tests/fork/conftest.py b/tests/fork/conftest.py index b659e3505..2fb98a8fc 100644 --- a/tests/fork/conftest.py +++ b/tests/fork/conftest.py @@ -1,4 +1,5 @@ import json +import logging import subprocess import time from contextlib import contextmanager @@ -6,6 +7,7 @@ from typing import cast, get_args import pytest +import xdist from _pytest.nodes import Item from eth_account import Account from faker.proxy import Faker @@ -15,7 +17,7 @@ from web3_multi_provider import MultiProvider from src import variables -from src.main import ipfs_providers, logger +from src.main import ipfs_providers from src.modules.submodules.consensus import ConsensusModule from src.modules.submodules.oracle_module import BaseModule from src.modules.submodules.types import FrameConfig @@ -36,7 +38,7 @@ from src.web3py.contract_tweak import tweak_w3_contracts from src.web3py.extensions import KeysAPIClientModule, LazyCSM, LidoContracts, LidoValidatorsProvider, TransactionUtils -logger = logger.getChild("fork") +logger = logging.getLogger('fork_tests') class TestRunningException(Exception): @@ -56,6 +58,12 @@ def pytest_collection_modifyitems(items: list[Item]): ) +def pytest_sessionfinish(session, exitstatus): + if xdist.is_xdist_worker(session): + return + subprocess.run(['rm', '-rf', './testruns'], check=True) + + # # Global # @@ -97,10 +105,15 @@ def set_cache_path(monkeypatch, testrun_path): yield -@pytest.fixture -def testrun_path(worker_id, testrun_uid): - path = f"./testrun_{worker_id}_{testrun_uid}" - subprocess.run(['mkdir', path], check=True) +@pytest.fixture(scope='session') +def testruns_folder_path(): + return Path("./testruns") + + +@pytest.fixture() +def testrun_path(testruns_folder_path, worker_id, testrun_uid): + path = testruns_folder_path / f"{worker_id}_{testrun_uid}" + subprocess.run(['mkdir', '-p', path], check=True) yield path subprocess.run(['rm', '-rf', path], check=True) @@ -194,12 +207,16 @@ def blockstamp_for_forking( @pytest.fixture() -def forked_el_client(blockstamp_for_forking: BlockStamp, testrun_path: str): - port = Faker().random_int(min=10000, max=20000) +def anvil_port(): + return Faker().random_int(min=10000, max=20000) + + +@pytest.fixture() +def forked_el_client(blockstamp_for_forking: BlockStamp, testrun_path: str, anvil_port: int): cli_params = [ 'anvil', '--port', - str(port), + str(anvil_port), '--config-out', f'{testrun_path}/localhost.json', '--auto-impersonate', @@ -210,8 +227,8 @@ def forked_el_client(blockstamp_for_forking: BlockStamp, testrun_path: str): ] with subprocess.Popen(cli_params, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT) as process: time.sleep(5) - logger.info(f"TESTRUN Started fork on {port=} from {blockstamp_for_forking.block_number=}") - web3 = Web3(MultiProvider([f'http://127.0.0.1:{port}'], request_kwargs={'timeout': 5 * 60})) + logger.info(f"TESTRUN Started fork on {anvil_port=} from {blockstamp_for_forking.block_number=}") + web3 = Web3(MultiProvider([f'http://127.0.0.1:{anvil_port}'], request_kwargs={'timeout': 5 * 60})) tweak_w3_contracts(web3) web3.middleware_onion.add(construct_simple_cache_middleware()) web3.provider.make_request(RPCEndpoint('anvil_setBlockTimestampInterval'), [12]) diff --git a/tests/fork/test_csm_oracle_cycle.py b/tests/fork/test_csm_oracle_cycle.py index c36d99d70..d7cc7e2a3 100644 --- a/tests/fork/test_csm_oracle_cycle.py +++ b/tests/fork/test_csm_oracle_cycle.py @@ -1,10 +1,15 @@ +import os +import subprocess +from pathlib import Path + import pytest from src.modules.csm.csm import CSOracle from src.modules.submodules.types import FrameConfig from src.utils.range import sequence from src.web3py.types import Web3 -from tests.fork.conftest import first_slot_of_epoch +from tests.fork.conftest import first_slot_of_epoch, logger +from tests.fork.utils.lock import LockedDir @pytest.fixture() @@ -13,8 +18,90 @@ def hash_consensus_bin(): yield f.read() +@pytest.fixture(scope='session') +def csm_repo_path(testruns_folder_path): + return Path(testruns_folder_path) / "community-staking-module" + + +@pytest.fixture(scope='session') +def prepared_csm_repo(testruns_folder_path, csm_repo_path): + + if os.environ.get("GITHUB_ACTIONS") == "true": + # CI should have the repo cloned and prepared + if os.path.exists(csm_repo_path): + return csm_repo_path + raise ValueError("No cloned community-staking-module repo found, but running in CI. Fix the workflow.") + + original_dir = os.getcwd() + + with LockedDir(testruns_folder_path): + if not os.path.exists(csm_repo_path / ".prepared"): + logger.info("TESTRUN Cloning community-staking-module repo") + subprocess.run( + ["git", "clone", "https://github.com/lidofinance/community-staking-module", csm_repo_path], check=True + ) + os.chdir(csm_repo_path) + subprocess.run(["git", "checkout", "develop"], check=True) + subprocess.run(["just", "deps"], check=True) + subprocess.run(["just", "build"], check=True) + subprocess.run(["touch", ".prepared"], check=True) + os.chdir(original_dir) + + return csm_repo_path + + +@pytest.fixture() +def update_csm_to_v2(accounts_from_fork, forked_el_client: Web3, anvil_port: int, prepared_csm_repo: Path): + original_dir = os.getcwd() + + chain = 'mainnet' + + logger.info("TESTRUN Deploying CSM v2") + _, pks = accounts_from_fork + deployer, *_ = pks + + os.chdir(prepared_csm_repo) + + with subprocess.Popen( + ['just', '_deploy-impl', '--broadcast'], + env={ + **os.environ, + "ANVIL_PORT": str(anvil_port), + 'DEPLOYER_PRIVATE_KEY': deployer, + 'CHAIN': chain, + }, + stdout=subprocess.DEVNULL, + stderr=subprocess.STDOUT, + ) as process: + process.wait() + assert process.returncode == 0, "Failed to deploy CSM v2" + logger.info("TESTRUN Deployed CSM v2") + + logger.info("TESTRUN Updating to CSM v2") + with subprocess.Popen( + ['just', "vote-upgrade"], + env={ + **os.environ, + 'CHAIN': chain, + "ANVIL_PORT": str(anvil_port), + "RPC_URL": f"http://127.0.0.1:{anvil_port}", # FIXME: actually unused by the script, remove when fixed + 'DEPLOY_CONFIG': f'./artifacts/{chain}/deploy-{chain}.json', + 'UPGRADE_CONFIG': f'./artifacts/local/upgrade-{chain}.json', + }, + stdout=subprocess.DEVNULL, + stderr=subprocess.STDOUT, + ) as process: + process.wait() + assert process.returncode == 0, "Failed to update to CSM v2" + logger.info("TESTRUN Updated to CSM v2") + + os.chdir(original_dir) + # TODO: update ABIs in `assets` folder? + forked_el_client.provider.make_request("anvil_autoImpersonateAccount", [True]) + + @pytest.fixture() -def csm_module(web3: Web3): +def csm_module(web3: Web3, update_csm_to_v2): yield CSOracle(web3) diff --git a/tests/fork/utils/lock.py b/tests/fork/utils/lock.py new file mode 100644 index 000000000..773a6025e --- /dev/null +++ b/tests/fork/utils/lock.py @@ -0,0 +1,23 @@ +import os +import subprocess +import time +from pathlib import Path + + +class LockedDir: + def __init__(self, path): + self.path = path + self._lock_file = ".locked" + self._lock_file_path = Path(self.path) / self._lock_file + + def __enter__(self): + while os.path.exists(self._lock_file_path): + time.sleep(1) + subprocess.run(["mkdir", "-p", self.path], check=True) + subprocess.run(["touch", self._lock_file_path], check=True) + + def __exit__(self, exc_type, exc_val, exc_tb): + subprocess.run(["rm", self._lock_file_path], check=True) + + def is_unlocked(self): + return not os.path.exists(self._lock_file_path) From 05555621e01ce466edaab18b4ff5677d5e8393b9 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Sat, 22 Feb 2025 10:02:57 +0100 Subject: [PATCH 041/162] fix: blockstamp_for_forking --- tests/fork/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fork/conftest.py b/tests/fork/conftest.py index 2fb98a8fc..75d458adb 100644 --- a/tests/fork/conftest.py +++ b/tests/fork/conftest.py @@ -191,7 +191,7 @@ def frame_config(initial_epoch, epochs_per_frame, fast_lane_length_slots): return _frame_config -@pytest.fixture(params=[-2], ids=["fork 2 epochs before initial epoch"]) +@pytest.fixture(params=[-4], ids=["fork 4 epochs before initial epoch"]) def blockstamp_for_forking( request, frame_config: FrameConfig, real_cl_client: ConsensusClient, real_finalized_slot: SlotNumber ) -> BlockStamp: From 811be112edb381937622f88d1990df493f403eb2 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Mon, 24 Feb 2025 10:08:48 +0100 Subject: [PATCH 042/162] fix: ignore --- tests/fork/conftest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/fork/conftest.py b/tests/fork/conftest.py index 75d458adb..de125d27c 100644 --- a/tests/fork/conftest.py +++ b/tests/fork/conftest.py @@ -40,6 +40,8 @@ logger = logging.getLogger('fork_tests') +# pylint: disable=logging-fstring-interpolation + class TestRunningException(Exception): pass From 34531cdd554a6acdf70a63a1c1986661ec364f76 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Mon, 24 Feb 2025 12:51:34 +0100 Subject: [PATCH 043/162] fix: some TODOs --- src/modules/csm/distribution.py | 8 ++------ src/modules/csm/log.py | 2 -- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/modules/csm/distribution.py b/src/modules/csm/distribution.py index 7d3919055..a6ca9e26d 100644 --- a/src/modules/csm/distribution.py +++ b/src/modules/csm/distribution.py @@ -95,11 +95,6 @@ def _calculate_distribution_in_frame( network_perf = self._get_network_performance(frame) - # TODO: get curves count from the contract - # curves_count = self.w3.csm.accounting.get_curves_count(blockstamp.block_hash) - curves_count = 2 - _cached_curve_params = lru_cache(maxsize=curves_count)(self._get_curve_params) - stuck_operators = self._get_stuck_operators(frame, blockstamp) for (_, no_id), validators in operators_to_validators.items(): logger.info({"msg": f"Calculating distribution for {no_id=}"}) @@ -109,7 +104,7 @@ def _calculate_distribution_in_frame( continue curve_id = self.w3.csm.accounting.get_bond_curve_id(no_id, blockstamp.block_hash) - perf_coeffs, perf_leeway, reward_share = _cached_curve_params(curve_id, blockstamp) + perf_coeffs, perf_leeway, reward_share = self._get_curve_params(curve_id, blockstamp) sorted_validators = sorted(validators, key=lambda v: v.index) for key_number, validator in enumerate(sorted_validators): @@ -147,6 +142,7 @@ def _calculate_distribution_in_frame( return rewards_distribution, log + @lru_cache() def _get_curve_params(self, curve_id: int, blockstamp: ReferenceBlockStamp): perf_coeffs = self.w3.csm.params.get_performance_coefficients(curve_id, blockstamp.block_hash) perf_leeway_data = self.w3.csm.params.get_performance_leeway_data(curve_id, blockstamp.block_hash) diff --git a/src/modules/csm/log.py b/src/modules/csm/log.py index 92d0c12e0..18dfdd895 100644 --- a/src/modules/csm/log.py +++ b/src/modules/csm/log.py @@ -10,8 +10,6 @@ class LogJSONEncoder(json.JSONEncoder): ... -# TODO: Should we log params? - @dataclass class ValidatorFrameSummary: From 3495efb5ca7dc65e6cbcda678f91f86784db525b Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Mon, 24 Feb 2025 15:47:49 +0100 Subject: [PATCH 044/162] feat: remove `stuck` from CSM --- src/modules/csm/distribution.py | 32 +------- src/modules/csm/log.py | 1 - .../execution/contracts/cs_module.py | 13 ---- src/web3py/extensions/csm.py | 25 ------- tests/modules/csm/test_csm_distribution.py | 26 ------- tests/modules/csm/test_csm_module.py | 74 ------------------- 6 files changed, 1 insertion(+), 170 deletions(-) diff --git a/src/modules/csm/distribution.py b/src/modules/csm/distribution.py index a6ca9e26d..04cc20aad 100644 --- a/src/modules/csm/distribution.py +++ b/src/modules/csm/distribution.py @@ -7,8 +7,7 @@ from src.modules.csm.types import Shares from src.providers.execution.contracts.cs_parameters_registry import PerformanceCoefficients from src.types import NodeOperatorId, ReferenceBlockStamp, EpochNumber, StakingModuleAddress -from src.utils.blockstamp import build_blockstamp -from src.utils.slot import get_reference_blockstamp, get_next_non_missed_slot +from src.utils.slot import get_reference_blockstamp from src.utils.web3converter import Web3Converter from src.web3py.extensions.lido_validators import LidoValidator, ValidatorsByNodeOperator, StakingModule from src.web3py.types import Web3 @@ -95,13 +94,9 @@ def _calculate_distribution_in_frame( network_perf = self._get_network_performance(frame) - stuck_operators = self._get_stuck_operators(frame, blockstamp) for (_, no_id), validators in operators_to_validators.items(): logger.info({"msg": f"Calculating distribution for {no_id=}"}) log_operator = log.operators[no_id] - if no_id in stuck_operators: - log_operator.stuck = True - continue curve_id = self.w3.csm.accounting.get_bond_curve_id(no_id, blockstamp.block_hash) perf_coeffs, perf_leeway, reward_share = self._get_curve_params(curve_id, blockstamp) @@ -156,31 +151,6 @@ def _get_network_performance(self, frame: Frame) -> float: network_perf = PerformanceCoefficients().calc_performance(att_perf, prop_perf, sync_perf) return network_perf - def _get_stuck_operators(self, frame: Frame, frame_blockstamp: ReferenceBlockStamp) -> set[NodeOperatorId]: - l_epoch, _ = frame - l_ref_slot = self.converter.get_epoch_first_slot(l_epoch) - # NOTE: r_block is guaranteed to be <= ref_slot, and the check - # in the inner frames assures the l_block <= r_block. - l_blockstamp = build_blockstamp( - get_next_non_missed_slot( - self.w3.cc, - l_ref_slot, - frame_blockstamp.slot_number, - ) - ) - - digests = self.w3.lido_contracts.staking_router.get_all_node_operator_digests( - self.staking_module, l_blockstamp.block_hash - ) - if not digests: - logger.warning("No CSM digest at blockstamp=%s, module was not added yet?", l_blockstamp) - stuck_from_digests = (no.id for no in digests if no.stuck_validators_count > 0) - stuck_from_events = self.w3.csm.get_operators_with_stucks_in_range( - l_blockstamp.block_hash, - frame_blockstamp.block_hash, - ) - return set(stuck_from_digests) | set(stuck_from_events) - @staticmethod def process_validator_duties( validator: LidoValidator, diff --git a/src/modules/csm/log.py b/src/modules/csm/log.py index 18dfdd895..af115596d 100644 --- a/src/modules/csm/log.py +++ b/src/modules/csm/log.py @@ -25,7 +25,6 @@ class ValidatorFrameSummary: @dataclass class OperatorFrameSummary: distributed: int = 0 - stuck: bool = False performance_coefficients: PerformanceCoefficients = field(default_factory=PerformanceCoefficients) validators: dict[ValidatorIndex, ValidatorFrameSummary] = field(default_factory=lambda: defaultdict(ValidatorFrameSummary)) diff --git a/src/providers/execution/contracts/cs_module.py b/src/providers/execution/contracts/cs_module.py index 3b1558a5e..94ba9eb4f 100644 --- a/src/providers/execution/contracts/cs_module.py +++ b/src/providers/execution/contracts/cs_module.py @@ -12,19 +12,6 @@ logger = logging.getLogger(__name__) -class NodeOperatorSummary(NamedTuple): - """getNodeOperatorSummary response, @see IStakingModule.sol""" - - targetLimitMode: int - targetValidatorsCount: int - stuckValidatorsCount: int - refundedValidatorsCount: int - stuckPenaltyEndTimestamp: int - totalExitedValidators: int - totalDepositedValidators: int - depositableValidatorsCount: int - - class CSModuleContract(ContractInterface): abi_path = "./assets/CSModule.json" diff --git a/src/web3py/extensions/csm.py b/src/web3py/extensions/csm.py index 2f80a2b39..2ef550f11 100644 --- a/src/web3py/extensions/csm.py +++ b/src/web3py/extensions/csm.py @@ -55,31 +55,6 @@ def get_csm_tree_cid(self, blockstamp: BlockStamp) -> CID | None: return None return CIDv0(result) if is_cid_v0(result) else CIDv1(result) - def get_operators_with_stucks_in_range( - self, - l_block: BlockIdentifier, - r_block: BlockIdentifier, - ) -> Iterator[NodeOperatorId]: - """Returns node operators assumed to be stuck for the given frame (defined by the block identifiers)""" - - l_block_number = self.w3.eth.get_block(l_block).get("number", BlockNumber(0)) - r_block_number = self.w3.eth.get_block(r_block).get("number", BlockNumber(0)) - - by_no_id: Callable[[EventData], int] = lambda e: e["args"]["nodeOperatorId"] - - events = sorted( - get_events_in_range( - cast(ContractEvent, self.module.events.StuckSigningKeysCountChanged), - l_block_number, - r_block_number, - ), - key=by_no_id, - ) - - for no_id, group in groupby(events, key=by_no_id): - if any(e["args"]["stuckKeysCount"] > 0 for e in group): - yield NodeOperatorId(no_id) - def _load_contracts(self) -> None: try: self.module = cast( diff --git a/tests/modules/csm/test_csm_distribution.py b/tests/modules/csm/test_csm_distribution.py index 28c19398f..0d7ac5fa6 100644 --- a/tests/modules/csm/test_csm_distribution.py +++ b/tests/modules/csm/test_csm_distribution.py @@ -78,26 +78,6 @@ def test_calculate_distribution_handles_invalid_distribution(module): module.calculate_distribution(blockstamp) -def test_calculate_distribution_in_frame_handles_stuck_operator(module): - frame = Mock() - blockstamp = Mock() - rewards_to_distribute = UINT64_MAX - operators_to_validators = {(Mock(), NodeOperatorId(1)): [LidoValidatorFactory.build()]} - module.state = State() - module.state.data = {frame: defaultdict(DutyAccumulator)} - module.get_stuck_operators = Mock(return_value={NodeOperatorId(1)}) - module._get_performance_threshold = Mock() - - rewards_distribution, log = module._calculate_distribution_in_frame( - frame, blockstamp, rewards_to_distribute, operators_to_validators - ) - - assert rewards_distribution[NodeOperatorId(1)] == 0 - assert log.operators[NodeOperatorId(1)].stuck is True - assert log.operators[NodeOperatorId(1)].distributed == 0 - assert log.operators[NodeOperatorId(1)].validators == defaultdict(ValidatorFrameSummary) - - def test_calculate_distribution_in_frame_handles_no_any_duties(module): frame = Mock() blockstamp = Mock() @@ -109,7 +89,6 @@ def test_calculate_distribution_in_frame_handles_no_any_duties(module): module.state.att_data = {frame: defaultdict(DutyAccumulator)} module.state.prop_data = {frame: defaultdict(DutyAccumulator)} module.state.sync_data = {frame: defaultdict(DutyAccumulator)} - module.get_stuck_operators = Mock(return_value=set()) module._get_performance_threshold = Mock() rewards_distribution, log = module._calculate_distribution_in_frame( @@ -117,7 +96,6 @@ def test_calculate_distribution_in_frame_handles_no_any_duties(module): ) assert rewards_distribution[node_operator_id] == 0 - assert log.operators[node_operator_id].stuck is False assert log.operators[node_operator_id].distributed == 0 assert log.operators[node_operator_id].validators == defaultdict(ValidatorFrameSummary) @@ -137,7 +115,6 @@ def test_calculate_distribution_in_frame_handles_above_threshold_performance(mod module.state.att_data = {frame: {validator.index: attestation_duty}} module.state.prop_data = {frame: {validator.index: proposal_duty}} module.state.sync_data = {frame: {validator.index: sync_duty}} - module.get_stuck_operators = Mock(return_value=set()) module._get_performance_threshold = Mock(return_value=0.5) rewards_distribution, log = module._calculate_distribution_in_frame( @@ -145,7 +122,6 @@ def test_calculate_distribution_in_frame_handles_above_threshold_performance(mod ) assert rewards_distribution[node_operator_id] > 0 # no need to check exact value - assert log.operators[node_operator_id].stuck is False assert log.operators[node_operator_id].distributed > 0 assert log.operators[node_operator_id].validators[validator.index].attestation_duty == attestation_duty assert log.operators[node_operator_id].validators[validator.index].proposal_duty == proposal_duty @@ -167,7 +143,6 @@ def test_calculate_distribution_in_frame_handles_below_threshold_performance(mod module.state.att_data = {frame: {validator.index: attestation_duty}} module.state.prop_data = {frame: {validator.index: proposal_duty}} module.state.sync_data = {frame: {validator.index: sync_duty}} - module.get_stuck_operators = Mock(return_value=set()) module._get_performance_threshold = Mock(return_value=0.5) rewards_distribution, log = module._calculate_distribution_in_frame( @@ -175,7 +150,6 @@ def test_calculate_distribution_in_frame_handles_below_threshold_performance(mod ) assert rewards_distribution[node_operator_id] == 0 - assert log.operators[node_operator_id].stuck is False assert log.operators[node_operator_id].distributed == 0 assert log.operators[node_operator_id].validators[validator.index].attestation_duty == attestation_duty assert log.operators[node_operator_id].validators[validator.index].proposal_duty == proposal_duty diff --git a/tests/modules/csm/test_csm_module.py b/tests/modules/csm/test_csm_module.py index ee096b64d..874254f6e 100644 --- a/tests/modules/csm/test_csm_module.py +++ b/tests/modules/csm/test_csm_module.py @@ -38,80 +38,6 @@ def module(web3, csm: CSM): def test_init(module: CSOracle): assert module - -def test_get_stuck_operators(module: CSOracle, csm: CSM): - module.module = Mock() # type: ignore - module.w3.cc = Mock() - module.w3.lido_validators = Mock() - module.w3.lido_contracts = Mock() - module.w3.lido_contracts.staking_router.get_all_node_operator_digests = Mock( - return_value=[ - Mock(id=0, stuck_validators_count=0), - Mock(id=1, stuck_validators_count=0), - Mock(id=2, stuck_validators_count=1), - Mock(id=3, stuck_validators_count=0), - Mock(id=4, stuck_validators_count=100500), - Mock(id=5, stuck_validators_count=100), - Mock(id=6, stuck_validators_count=0), - ] - ) - - module.w3.csm.get_operators_with_stucks_in_range = Mock( - return_value=[NodeOperatorId(2), NodeOperatorId(4), NodeOperatorId(6), NodeOperatorId(1337)] - ) - - module.get_epochs_range_to_process = Mock(return_value=(69, 100)) - module.converter = Mock() - module.converter.get_epoch_first_slot = Mock(return_value=lambda epoch: epoch * 32) - - l_blockstamp = Mock() - blockstamp = Mock() - l_blockstamp.block_hash = "0x01" - blockstamp.slot_number = "1" - blockstamp.block_hash = "0x02" - - with patch('src.modules.csm.csm.build_blockstamp', return_value=l_blockstamp): - with patch('src.modules.csm.csm.get_next_non_missed_slot', return_value=Mock()): - stuck = module.get_stuck_operators(frame=(69, 100), frame_blockstamp=blockstamp) - - assert stuck == {NodeOperatorId(2), NodeOperatorId(4), NodeOperatorId(5), NodeOperatorId(6), NodeOperatorId(1337)} - - -def test_get_stuck_operators_left_border_before_enact(module: CSOracle, csm: CSM, caplog: pytest.LogCaptureFixture): - module.module = Mock() # type: ignore - module.w3.cc = Mock() - module.w3.lido_validators = Mock() - module.w3.lido_contracts = Mock() - module.w3.lido_contracts.staking_router.get_all_node_operator_digests = Mock(return_value=[]) - - module.w3.csm.get_operators_with_stucks_in_range = Mock( - return_value=[ - NodeOperatorId(2), - NodeOperatorId(4), - NodeOperatorId(6), - ] - ) - - module.get_epochs_range_to_process = Mock(return_value=(69, 100)) - module.converter = Mock() - module.converter.get_epoch_first_slot = Mock(return_value=lambda epoch: epoch * 32) - - l_blockstamp = BlockStampFactory.build() - blockstamp = BlockStampFactory.build() - - with patch('src.modules.csm.csm.build_blockstamp', return_value=l_blockstamp): - with patch('src.modules.csm.csm.get_next_non_missed_slot', return_value=Mock()): - stuck = module.get_stuck_operators(frame=(69, 100), frame_blockstamp=blockstamp) - - assert stuck == { - NodeOperatorId(2), - NodeOperatorId(4), - NodeOperatorId(6), - } - - assert caplog.messages[0].startswith("No CSM digest at blockstamp") - - # Static functions you were dreaming of for so long. From 2988bfc973ad1226072b736bb206be6ef310830b Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Tue, 25 Feb 2025 10:19:18 +0100 Subject: [PATCH 045/162] remove: delete stucks from CSM --- src/modules/csm/csm.py | 40 ++------- src/modules/csm/log.py | 1 - .../execution/contracts/cs_module.py | 14 --- src/web3py/extensions/csm.py | 33 +------ tests/modules/csm/test_csm_distribution.py | 28 +----- tests/modules/csm/test_csm_module.py | 85 ++----------------- tests/modules/csm/test_log.py | 1 - 7 files changed, 13 insertions(+), 189 deletions(-) diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index e9e042172..4c20ceff9 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -13,7 +13,7 @@ from src.metrics.prometheus.duration_meter import duration_meter from src.modules.csm.checkpoint import FrameCheckpointProcessor, FrameCheckpointsIterator, MinStepIsNotReached from src.modules.csm.log import FramePerfLog, OperatorFrameSummary -from src.modules.csm.state import State, Frame, AttestationsAccumulator +from src.modules.csm.state import AttestationsAccumulator, Frame, State from src.modules.csm.tree import Tree from src.modules.csm.types import ReportData, Shares from src.modules.submodules.consensus import ConsensusModule @@ -29,11 +29,10 @@ SlotNumber, StakingModuleAddress, ) -from src.utils.blockstamp import build_blockstamp from src.utils.cache import global_lru_cache as lru_cache -from src.utils.slot import get_next_non_missed_slot, get_reference_blockstamp +from src.utils.slot import get_reference_blockstamp from src.utils.web3converter import Web3Converter -from src.web3py.extensions.lido_validators import NodeOperatorId, StakingModule, ValidatorsByNodeOperator, LidoValidator +from src.web3py.extensions.lido_validators import LidoValidator, NodeOperatorId, StakingModule, ValidatorsByNodeOperator from src.web3py.types import Web3 logger = logging.getLogger(__name__) @@ -277,19 +276,15 @@ def _calculate_distribution_in_frame( frame: Frame, blockstamp: ReferenceBlockStamp, rewards_to_distribute: int, - operators_to_validators: ValidatorsByNodeOperator + operators_to_validators: ValidatorsByNodeOperator, ): threshold = self._get_performance_threshold(frame, blockstamp) log = FramePerfLog(blockstamp, frame, threshold) participation_shares: defaultdict[NodeOperatorId, int] = defaultdict(int) - stuck_operators = self.get_stuck_operators(frame, blockstamp) for (_, no_id), validators in operators_to_validators.items(): log_operator = log.operators[no_id] - if no_id in stuck_operators: - log_operator.stuck = True - continue for validator in validators: duty = self.state.data[frame].get(validator.index) self.process_validator_duty(validator, duty, threshold, participation_shares, log_operator) @@ -315,7 +310,7 @@ def process_validator_duty( attestation_duty: AttestationsAccumulator | None, threshold: float, participation_shares: defaultdict[NodeOperatorId, int], - log_operator: OperatorFrameSummary + log_operator: OperatorFrameSummary, ): if attestation_duty is None: # It's possible that the validator is not assigned to any duty, hence it's performance @@ -368,31 +363,6 @@ def get_accumulated_rewards(self, cid: CID, root: HexBytes) -> Iterator[tuple[No for v in tree.tree.values: yield v["value"] - def get_stuck_operators(self, frame: Frame, frame_blockstamp: ReferenceBlockStamp) -> set[NodeOperatorId]: - l_epoch, _ = frame - l_ref_slot = self.converter(frame_blockstamp).get_epoch_first_slot(l_epoch) - # NOTE: r_block is guaranteed to be <= ref_slot, and the check - # in the inner frames assures the l_block <= r_block. - l_blockstamp = build_blockstamp( - get_next_non_missed_slot( - self.w3.cc, - l_ref_slot, - frame_blockstamp.slot_number, - ) - ) - - digests = self.w3.lido_contracts.staking_router.get_all_node_operator_digests( - self.staking_module, l_blockstamp.block_hash - ) - if not digests: - logger.warning("No CSM digest at blockstamp=%s, module was not added yet?", l_blockstamp) - stuck_from_digests = (no.id for no in digests if no.stuck_validators_count > 0) - stuck_from_events = self.w3.csm.get_operators_with_stucks_in_range( - l_blockstamp.block_hash, - frame_blockstamp.block_hash, - ) - return set(stuck_from_digests) | set(stuck_from_events) - def make_tree(self, shares: dict[NodeOperatorId, Shares]) -> Tree: if not shares: raise ValueError("No shares to build a tree") diff --git a/src/modules/csm/log.py b/src/modules/csm/log.py index 29ab24902..02b32fc50 100644 --- a/src/modules/csm/log.py +++ b/src/modules/csm/log.py @@ -20,7 +20,6 @@ class ValidatorFrameSummary: class OperatorFrameSummary: distributed: int = 0 validators: dict[ValidatorIndex, ValidatorFrameSummary] = field(default_factory=lambda: defaultdict(ValidatorFrameSummary)) - stuck: bool = False @dataclass diff --git a/src/providers/execution/contracts/cs_module.py b/src/providers/execution/contracts/cs_module.py index 3b1558a5e..2ac59f55e 100644 --- a/src/providers/execution/contracts/cs_module.py +++ b/src/providers/execution/contracts/cs_module.py @@ -1,5 +1,4 @@ import logging -from typing import NamedTuple from eth_typing import ChecksumAddress from web3 import Web3 @@ -12,19 +11,6 @@ logger = logging.getLogger(__name__) -class NodeOperatorSummary(NamedTuple): - """getNodeOperatorSummary response, @see IStakingModule.sol""" - - targetLimitMode: int - targetValidatorsCount: int - stuckValidatorsCount: int - refundedValidatorsCount: int - stuckPenaltyEndTimestamp: int - totalExitedValidators: int - totalDepositedValidators: int - depositableValidatorsCount: int - - class CSModuleContract(ContractInterface): abi_path = "./assets/CSModule.json" diff --git a/src/web3py/extensions/csm.py b/src/web3py/extensions/csm.py index 2f80a2b39..973910a78 100644 --- a/src/web3py/extensions/csm.py +++ b/src/web3py/extensions/csm.py @@ -1,17 +1,13 @@ import logging from functools import partial -from itertools import groupby from time import sleep -from typing import Callable, Iterator, cast +from typing import cast -from eth_typing import BlockNumber from hexbytes import HexBytes from lazy_object_proxy import Proxy from web3 import Web3 -from web3.contract.contract import ContractEvent from web3.exceptions import Web3Exception from web3.module import Module -from web3.types import BlockIdentifier, EventData from src import variables from src.metrics.prometheus.business import FRAME_PREV_REPORT_REF_SLOT @@ -22,8 +18,6 @@ from src.providers.execution.contracts.cs_parameters_registry import CSParametersRegistryContract from src.providers.ipfs import CID, CIDv0, CIDv1, is_cid_v0 from src.types import BlockStamp, SlotNumber -from src.utils.events import get_events_in_range -from src.web3py.extensions.lido_validators import NodeOperatorId logger = logging.getLogger(__name__) @@ -55,31 +49,6 @@ def get_csm_tree_cid(self, blockstamp: BlockStamp) -> CID | None: return None return CIDv0(result) if is_cid_v0(result) else CIDv1(result) - def get_operators_with_stucks_in_range( - self, - l_block: BlockIdentifier, - r_block: BlockIdentifier, - ) -> Iterator[NodeOperatorId]: - """Returns node operators assumed to be stuck for the given frame (defined by the block identifiers)""" - - l_block_number = self.w3.eth.get_block(l_block).get("number", BlockNumber(0)) - r_block_number = self.w3.eth.get_block(r_block).get("number", BlockNumber(0)) - - by_no_id: Callable[[EventData], int] = lambda e: e["args"]["nodeOperatorId"] - - events = sorted( - get_events_in_range( - cast(ContractEvent, self.module.events.StuckSigningKeysCountChanged), - l_block_number, - r_block_number, - ), - key=by_no_id, - ) - - for no_id, group in groupby(events, key=by_no_id): - if any(e["args"]["stuckKeysCount"] > 0 for e in group): - yield NodeOperatorId(no_id) - def _load_contracts(self) -> None: try: self.module = cast( diff --git a/tests/modules/csm/test_csm_distribution.py b/tests/modules/csm/test_csm_distribution.py index f277f6eb8..1fdbf0e6b 100644 --- a/tests/modules/csm/test_csm_distribution.py +++ b/tests/modules/csm/test_csm_distribution.py @@ -5,7 +5,7 @@ from web3.types import Wei from src.constants import UINT64_MAX -from src.modules.csm.csm import CSOracle, CSMError +from src.modules.csm.csm import CSMError, CSOracle from src.modules.csm.log import ValidatorFrameSummary from src.modules.csm.state import AttestationsAccumulator, State from src.types import NodeOperatorId, ValidatorIndex @@ -78,26 +78,6 @@ def test_calculate_distribution_handles_invalid_distribution(module): module.calculate_distribution(blockstamp) -def test_calculate_distribution_in_frame_handles_stuck_operator(module): - frame = Mock() - blockstamp = Mock() - rewards_to_distribute = UINT64_MAX - operators_to_validators = {(Mock(), NodeOperatorId(1)): [LidoValidatorFactory.build()]} - module.state = State() - module.state.data = {frame: defaultdict(AttestationsAccumulator)} - module.get_stuck_operators = Mock(return_value={NodeOperatorId(1)}) - module._get_performance_threshold = Mock() - - rewards_distribution, log = module._calculate_distribution_in_frame( - frame, blockstamp, rewards_to_distribute, operators_to_validators - ) - - assert rewards_distribution[NodeOperatorId(1)] == 0 - assert log.operators[NodeOperatorId(1)].stuck is True - assert log.operators[NodeOperatorId(1)].distributed == 0 - assert log.operators[NodeOperatorId(1)].validators == defaultdict(ValidatorFrameSummary) - - def test_calculate_distribution_in_frame_handles_no_attestation_duty(module): frame = Mock() blockstamp = Mock() @@ -107,7 +87,6 @@ def test_calculate_distribution_in_frame_handles_no_attestation_duty(module): operators_to_validators = {(Mock(), node_operator_id): [validator]} module.state = State() module.state.data = {frame: defaultdict(AttestationsAccumulator)} - module.get_stuck_operators = Mock(return_value=set()) module._get_performance_threshold = Mock() rewards_distribution, log = module._calculate_distribution_in_frame( @@ -115,7 +94,6 @@ def test_calculate_distribution_in_frame_handles_no_attestation_duty(module): ) assert rewards_distribution[node_operator_id] == 0 - assert log.operators[node_operator_id].stuck is False assert log.operators[node_operator_id].distributed == 0 assert log.operators[node_operator_id].validators == defaultdict(ValidatorFrameSummary) @@ -131,7 +109,6 @@ def test_calculate_distribution_in_frame_handles_above_threshold_performance(mod module.state = State() attestation_duty = AttestationsAccumulator(assigned=10, included=6) module.state.data = {frame: {validator.index: attestation_duty}} - module.get_stuck_operators = Mock(return_value=set()) module._get_performance_threshold = Mock(return_value=0.5) rewards_distribution, log = module._calculate_distribution_in_frame( @@ -139,7 +116,6 @@ def test_calculate_distribution_in_frame_handles_above_threshold_performance(mod ) assert rewards_distribution[node_operator_id] > 0 # no need to check exact value - assert log.operators[node_operator_id].stuck is False assert log.operators[node_operator_id].distributed > 0 assert log.operators[node_operator_id].validators[validator.index].attestation_duty == attestation_duty @@ -155,7 +131,6 @@ def test_calculate_distribution_in_frame_handles_below_threshold_performance(mod module.state = State() attestation_duty = AttestationsAccumulator(assigned=10, included=5) module.state.data = {frame: {validator.index: attestation_duty}} - module.get_stuck_operators = Mock(return_value=set()) module._get_performance_threshold = Mock(return_value=0.5) rewards_distribution, log = module._calculate_distribution_in_frame( @@ -163,7 +138,6 @@ def test_calculate_distribution_in_frame_handles_below_threshold_performance(mod ) assert rewards_distribution[node_operator_id] == 0 - assert log.operators[node_operator_id].stuck is False assert log.operators[node_operator_id].distributed == 0 assert log.operators[node_operator_id].validators[validator.index].attestation_duty == attestation_duty diff --git a/tests/modules/csm/test_csm_module.py b/tests/modules/csm/test_csm_module.py index ee096b64d..ac5812227 100644 --- a/tests/modules/csm/test_csm_module.py +++ b/tests/modules/csm/test_csm_module.py @@ -1,8 +1,8 @@ import logging from collections import defaultdict from dataclasses import dataclass -from typing import NoReturn, Iterable, Literal, Type -from unittest.mock import Mock, patch, PropertyMock +from typing import Iterable, Literal, NoReturn, Type +from unittest.mock import Mock, PropertyMock, patch import pytest from hexbytes import HexBytes @@ -12,11 +12,11 @@ from src.modules.csm.state import State from src.modules.csm.tree import Tree from src.modules.submodules.oracle_module import ModuleExecuteDelay -from src.modules.submodules.types import CurrentFrame, ZERO_HASH -from src.providers.ipfs import CIDv0, CID -from src.types import NodeOperatorId, SlotNumber, StakingModuleId +from src.modules.submodules.types import ZERO_HASH, CurrentFrame +from src.providers.ipfs import CID, CIDv0 +from src.types import NodeOperatorId, SlotNumber from src.web3py.extensions.csm import CSM -from tests.factory.blockstamp import BlockStampFactory, ReferenceBlockStampFactory +from tests.factory.blockstamp import ReferenceBlockStampFactory from tests.factory.configs import ChainConfigFactory, FrameConfigFactory @@ -39,79 +39,6 @@ def test_init(module: CSOracle): assert module -def test_get_stuck_operators(module: CSOracle, csm: CSM): - module.module = Mock() # type: ignore - module.w3.cc = Mock() - module.w3.lido_validators = Mock() - module.w3.lido_contracts = Mock() - module.w3.lido_contracts.staking_router.get_all_node_operator_digests = Mock( - return_value=[ - Mock(id=0, stuck_validators_count=0), - Mock(id=1, stuck_validators_count=0), - Mock(id=2, stuck_validators_count=1), - Mock(id=3, stuck_validators_count=0), - Mock(id=4, stuck_validators_count=100500), - Mock(id=5, stuck_validators_count=100), - Mock(id=6, stuck_validators_count=0), - ] - ) - - module.w3.csm.get_operators_with_stucks_in_range = Mock( - return_value=[NodeOperatorId(2), NodeOperatorId(4), NodeOperatorId(6), NodeOperatorId(1337)] - ) - - module.get_epochs_range_to_process = Mock(return_value=(69, 100)) - module.converter = Mock() - module.converter.get_epoch_first_slot = Mock(return_value=lambda epoch: epoch * 32) - - l_blockstamp = Mock() - blockstamp = Mock() - l_blockstamp.block_hash = "0x01" - blockstamp.slot_number = "1" - blockstamp.block_hash = "0x02" - - with patch('src.modules.csm.csm.build_blockstamp', return_value=l_blockstamp): - with patch('src.modules.csm.csm.get_next_non_missed_slot', return_value=Mock()): - stuck = module.get_stuck_operators(frame=(69, 100), frame_blockstamp=blockstamp) - - assert stuck == {NodeOperatorId(2), NodeOperatorId(4), NodeOperatorId(5), NodeOperatorId(6), NodeOperatorId(1337)} - - -def test_get_stuck_operators_left_border_before_enact(module: CSOracle, csm: CSM, caplog: pytest.LogCaptureFixture): - module.module = Mock() # type: ignore - module.w3.cc = Mock() - module.w3.lido_validators = Mock() - module.w3.lido_contracts = Mock() - module.w3.lido_contracts.staking_router.get_all_node_operator_digests = Mock(return_value=[]) - - module.w3.csm.get_operators_with_stucks_in_range = Mock( - return_value=[ - NodeOperatorId(2), - NodeOperatorId(4), - NodeOperatorId(6), - ] - ) - - module.get_epochs_range_to_process = Mock(return_value=(69, 100)) - module.converter = Mock() - module.converter.get_epoch_first_slot = Mock(return_value=lambda epoch: epoch * 32) - - l_blockstamp = BlockStampFactory.build() - blockstamp = BlockStampFactory.build() - - with patch('src.modules.csm.csm.build_blockstamp', return_value=l_blockstamp): - with patch('src.modules.csm.csm.get_next_non_missed_slot', return_value=Mock()): - stuck = module.get_stuck_operators(frame=(69, 100), frame_blockstamp=blockstamp) - - assert stuck == { - NodeOperatorId(2), - NodeOperatorId(4), - NodeOperatorId(6), - } - - assert caplog.messages[0].startswith("No CSM digest at blockstamp") - - # Static functions you were dreaming of for so long. diff --git a/tests/modules/csm/test_log.py b/tests/modules/csm/test_log.py index c95ef93ca..39802dcc5 100644 --- a/tests/modules/csm/test_log.py +++ b/tests/modules/csm/test_log.py @@ -23,7 +23,6 @@ def log(ref_blockstamp: ReferenceBlockStamp, frame: tuple[EpochNumber, EpochNumb def test_fields_access(log: FramePerfLog): log.operators[NodeOperatorId(42)].validators["100500"].slashed = True - log.operators[NodeOperatorId(17)].stuck = True def test_log_encode(log: FramePerfLog): From b705e7a856800c5abb95a02133f836f875ea0bd9 Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Fri, 21 Feb 2025 13:03:53 +0100 Subject: [PATCH 046/162] feat: add CSStrikes contract to csm extension --- assets/CSFeeOracle.json | 2 +- assets/CSStrikes.json | 1 + .../execution/contracts/cs_fee_oracle.py | 15 ++++++++ .../execution/contracts/cs_strikes.py | 38 +++++++++++++++++++ src/web3py/extensions/csm.py | 11 ++++++ 5 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 assets/CSStrikes.json create mode 100644 src/providers/execution/contracts/cs_strikes.py diff --git a/assets/CSFeeOracle.json b/assets/CSFeeOracle.json index c9bd74471..419541b27 100644 --- a/assets/CSFeeOracle.json +++ b/assets/CSFeeOracle.json @@ -1 +1 @@ -[{"type":"constructor","inputs":[{"name":"secondsPerSlot","type":"uint256","internalType":"uint256"},{"name":"genesisTime","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"GENESIS_TIME","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"MANAGE_CONSENSUS_CONTRACT_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"MANAGE_CONSENSUS_VERSION_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_INFINITELY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RECOVERER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RESUME_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"SECONDS_PER_SLOT","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"SUBMIT_DATA_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"discardConsensusReport","inputs":[{"name":"refSlot","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"feeDistributor","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSFeeDistributor"}],"stateMutability":"view"},{"type":"function","name":"finalizeUpgradeV2","inputs":[{"name":"consensusVersion","type":"uint256","internalType":"uint256"},{"name":"strikesContract","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"getConsensusContract","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getConsensusReport","inputs":[],"outputs":[{"name":"hash","type":"bytes32","internalType":"bytes32"},{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"processingDeadlineTime","type":"uint256","internalType":"uint256"},{"name":"processingStarted","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"getConsensusVersion","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getContractVersion","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getLastProcessingRefSlot","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getResumeSinceTimestamp","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"admin","type":"address","internalType":"address"},{"name":"feeDistributorContract","type":"address","internalType":"address"},{"name":"strikesContract","type":"address","internalType":"address"},{"name":"consensusContract","type":"address","internalType":"address"},{"name":"consensusVersion","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"isPaused","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"pauseFor","inputs":[{"name":"duration","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"pauseUntil","inputs":[{"name":"pauseUntilInclusive","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC1155","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC20","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC721","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverEther","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resume","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setConsensusContract","inputs":[{"name":"addr","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setConsensusVersion","inputs":[{"name":"version","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setFeeDistributorContract","inputs":[{"name":"feeDistributorContract","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setStrikesContract","inputs":[{"name":"strikesContract","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"strikes","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSStrikes"}],"stateMutability":"view"},{"type":"function","name":"submitConsensusReport","inputs":[{"name":"reportHash","type":"bytes32","internalType":"bytes32"},{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"submitReportData","inputs":[{"name":"data","type":"tuple","internalType":"struct ICSFeeOracle.ReportData","components":[{"name":"consensusVersion","type":"uint256","internalType":"uint256"},{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"treeRoot","type":"bytes32","internalType":"bytes32"},{"name":"treeCid","type":"string","internalType":"string"},{"name":"logCid","type":"string","internalType":"string"},{"name":"distributed","type":"uint256","internalType":"uint256"},{"name":"rebate","type":"uint256","internalType":"uint256"},{"name":"strikesTreeRoot","type":"bytes32","internalType":"bytes32"},{"name":"strikesTreeCid","type":"string","internalType":"string"}]},{"name":"contractVersion","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"event","name":"ConsensusHashContractSet","inputs":[{"name":"addr","type":"address","indexed":true,"internalType":"address"},{"name":"prevAddr","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"ConsensusVersionSet","inputs":[{"name":"version","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"prevVersion","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ContractVersionSet","inputs":[{"name":"version","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC1155Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC20Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC721Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"EtherRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"FeeDistributorContractSet","inputs":[{"name":"feeDistributorContract","type":"address","indexed":false,"internalType":"address"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"Paused","inputs":[{"name":"duration","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ProcessingStarted","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"hash","type":"bytes32","indexed":false,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"ReportDiscarded","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"hash","type":"bytes32","indexed":false,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"ReportSubmitted","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"hash","type":"bytes32","indexed":false,"internalType":"bytes32"},{"name":"processingDeadlineTime","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Resumed","inputs":[],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"StETHSharesRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"StrikesContractSet","inputs":[{"name":"strikesContract","type":"address","indexed":false,"internalType":"address"}],"anonymous":false},{"type":"event","name":"WarnProcessingMissed","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"AddressCannotBeSame","inputs":[]},{"type":"error","name":"AddressCannotBeZero","inputs":[]},{"type":"error","name":"FailedToSendEther","inputs":[]},{"type":"error","name":"HashCannotBeZero","inputs":[]},{"type":"error","name":"InitialRefSlotCannotBeLessThanProcessingOne","inputs":[{"name":"initialRefSlot","type":"uint256","internalType":"uint256"},{"name":"processingRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"InvalidContractVersion","inputs":[]},{"type":"error","name":"InvalidContractVersionIncrement","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"NoConsensusReportToProcess","inputs":[]},{"type":"error","name":"NonZeroContractVersionOnInit","inputs":[]},{"type":"error","name":"NotAllowedToRecover","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"PauseUntilMustBeInFuture","inputs":[]},{"type":"error","name":"PausedExpected","inputs":[]},{"type":"error","name":"ProcessingDeadlineMissed","inputs":[{"name":"deadline","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"RefSlotAlreadyProcessing","inputs":[]},{"type":"error","name":"RefSlotCannotDecrease","inputs":[{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"prevRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"RefSlotMustBeGreaterThanProcessingOne","inputs":[{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"processingRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"ResumedExpected","inputs":[]},{"type":"error","name":"SafeCastOverflowedUintDowncast","inputs":[{"name":"bits","type":"uint8","internalType":"uint8"},{"name":"value","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"SecondsPerSlotCannotBeZero","inputs":[]},{"type":"error","name":"SenderIsNotTheConsensusContract","inputs":[]},{"type":"error","name":"SenderNotAllowed","inputs":[]},{"type":"error","name":"UnexpectedChainConfig","inputs":[]},{"type":"error","name":"UnexpectedConsensusVersion","inputs":[{"name":"expectedVersion","type":"uint256","internalType":"uint256"},{"name":"receivedVersion","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"UnexpectedContractVersion","inputs":[{"name":"expected","type":"uint256","internalType":"uint256"},{"name":"received","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"UnexpectedDataHash","inputs":[{"name":"consensusHash","type":"bytes32","internalType":"bytes32"},{"name":"receivedHash","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"UnexpectedRefSlot","inputs":[{"name":"consensusRefSlot","type":"uint256","internalType":"uint256"},{"name":"dataRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"VersionCannotBeSame","inputs":[]},{"type":"error","name":"VersionCannotBeZero","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroFeeDistributorAddress","inputs":[]},{"type":"error","name":"ZeroPauseDuration","inputs":[]},{"type":"error","name":"ZeroStrikesAddress","inputs":[]}] \ No newline at end of file +[{"type":"constructor","inputs":[{"name":"secondsPerSlot","type":"uint256","internalType":"uint256"},{"name":"genesisTime","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"GENESIS_TIME","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"MANAGE_CONSENSUS_CONTRACT_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"MANAGE_CONSENSUS_VERSION_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_INFINITELY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RECOVERER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RESUME_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"SECONDS_PER_SLOT","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"SUBMIT_DATA_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"discardConsensusReport","inputs":[{"name":"refSlot","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"feeDistributor","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSFeeDistributor"}],"stateMutability":"view"},{"type":"function","name":"finalizeUpgradeV2","inputs":[{"name":"consensusVersion","type":"uint256","internalType":"uint256"},{"name":"strikesContract","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"getConsensusContract","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getConsensusReport","inputs":[],"outputs":[{"name":"hash","type":"bytes32","internalType":"bytes32"},{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"processingDeadlineTime","type":"uint256","internalType":"uint256"},{"name":"processingStarted","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"getConsensusVersion","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getContractVersion","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getLastProcessingRefSlot","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getResumeSinceTimestamp","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"admin","type":"address","internalType":"address"},{"name":"feeDistributorContract","type":"address","internalType":"address"},{"name":"strikesContract","type":"address","internalType":"address"},{"name":"consensusContract","type":"address","internalType":"address"},{"name":"consensusVersion","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"isPaused","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"pauseFor","inputs":[{"name":"duration","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"pauseUntil","inputs":[{"name":"pauseUntilInclusive","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC1155","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC20","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC721","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverEther","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resume","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setConsensusContract","inputs":[{"name":"addr","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setConsensusVersion","inputs":[{"name":"version","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setFeeDistributorContract","inputs":[{"name":"feeDistributorContract","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setStrikesContract","inputs":[{"name":"strikesContract","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"strikes","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSStrikes"}],"stateMutability":"view"},{"type":"function","name":"submitConsensusReport","inputs":[{"name":"reportHash","type":"bytes32","internalType":"bytes32"},{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"submitReportData","inputs":[{"name":"data","type":"tuple","internalType":"struct ICSFeeOracle.ReportData","components":[{"name":"consensusVersion","type":"uint256","internalType":"uint256"},{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"treeRoot","type":"bytes32","internalType":"bytes32"},{"name":"treeCid","type":"string","internalType":"string"},{"name":"logCid","type":"string","internalType":"string"},{"name":"distributed","type":"uint256","internalType":"uint256"},{"name":"rebate","type":"uint256","internalType":"uint256"},{"name":"strikesTreeRoot","type":"bytes32","internalType":"bytes32"},{"name":"strikesTreeCid","type":"string","internalType":"string"}]},{"name":"contractVersion","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"event","name":"ConsensusHashContractSet","inputs":[{"name":"addr","type":"address","indexed":true,"internalType":"address"},{"name":"prevAddr","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"ConsensusVersionSet","inputs":[{"name":"version","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"prevVersion","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ContractVersionSet","inputs":[{"name":"version","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC1155Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC20Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC721Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"EtherRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"FeeDistributorContractSet","inputs":[{"name":"feeDistributorContract","type":"address","indexed":false,"internalType":"address"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"Paused","inputs":[{"name":"duration","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ProcessingStarted","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"hash","type":"bytes32","indexed":false,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"ReportDiscarded","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"hash","type":"bytes32","indexed":false,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"ReportSubmitted","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"hash","type":"bytes32","indexed":false,"internalType":"bytes32"},{"name":"processingDeadlineTime","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Resumed","inputs":[],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"StETHSharesRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"StrikesContractSet","inputs":[{"name":"strikesContract","type":"address","indexed":false,"internalType":"address"}],"anonymous":false},{"type":"event","name":"WarnProcessingMissed","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"AddressCannotBeSame","inputs":[]},{"type":"error","name":"AddressCannotBeZero","inputs":[]},{"type":"error","name":"FailedToSendEther","inputs":[]},{"type":"error","name":"HashCannotBeZero","inputs":[]},{"type":"error","name":"InitialRefSlotCannotBeLessThanProcessingOne","inputs":[{"name":"initialRefSlot","type":"uint256","internalType":"uint256"},{"name":"processingRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"InvalidContractVersion","inputs":[]},{"type":"error","name":"InvalidContractVersionIncrement","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"NoConsensusReportToProcess","inputs":[]},{"type":"error","name":"NonZeroContractVersionOnInit","inputs":[]},{"type":"error","name":"NotAllowedToRecover","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"PauseUntilMustBeInFuture","inputs":[]},{"type":"error","name":"PausedExpected","inputs":[]},{"type":"error","name":"ProcessingDeadlineMissed","inputs":[{"name":"deadline","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"RefSlotAlreadyProcessing","inputs":[]},{"type":"error","name":"RefSlotCannotDecrease","inputs":[{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"prevRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"RefSlotMustBeGreaterThanProcessingOne","inputs":[{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"processingRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"ResumedExpected","inputs":[]},{"type":"error","name":"SafeCastOverflowedUintDowncast","inputs":[{"name":"bits","type":"uint8","internalType":"uint8"},{"name":"value","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"SecondsPerSlotCannotBeZero","inputs":[]},{"type":"error","name":"SenderIsNotTheConsensusContract","inputs":[]},{"type":"error","name":"SenderNotAllowed","inputs":[]},{"type":"error","name":"UnexpectedChainConfig","inputs":[]},{"type":"error","name":"UnexpectedConsensusVersion","inputs":[{"name":"expectedVersion","type":"uint256","internalType":"uint256"},{"name":"receivedVersion","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"UnexpectedContractVersion","inputs":[{"name":"expected","type":"uint256","internalType":"uint256"},{"name":"received","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"UnexpectedDataHash","inputs":[{"name":"consensusHash","type":"bytes32","internalType":"bytes32"},{"name":"receivedHash","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"UnexpectedRefSlot","inputs":[{"name":"consensusRefSlot","type":"uint256","internalType":"uint256"},{"name":"dataRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"VersionCannotBeSame","inputs":[]},{"type":"error","name":"VersionCannotBeZero","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroFeeDistributorAddress","inputs":[]},{"type":"error","name":"ZeroPauseDuration","inputs":[]},{"type":"error","name":"ZeroStrikesAddress","inputs":[]}] diff --git a/assets/CSStrikes.json b/assets/CSStrikes.json new file mode 100644 index 000000000..d9a852d9b --- /dev/null +++ b/assets/CSStrikes.json @@ -0,0 +1 @@ +[{"type":"constructor","inputs":[{"name":"module","type":"address","internalType":"address"},{"name":"oracle","type":"address","internalType":"address"}],"stateMutability":"nonpayable"},{"type":"function","name":"MODULE","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSModule"}],"stateMutability":"view"},{"type":"function","name":"ORACLE","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"hashLeaf","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"pubkey","type":"bytes","internalType":"bytes"},{"name":"strikesData","type":"uint256[]","internalType":"uint256[]"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"pure"},{"type":"function","name":"processBadPerformanceProof","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"},{"name":"strikesData","type":"uint256[]","internalType":"uint256[]"},{"name":"proof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"processOracleReport","inputs":[{"name":"_treeRoot","type":"bytes32","internalType":"bytes32"},{"name":"_treeCid","type":"string","internalType":"string"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"treeCid","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"treeRoot","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"verifyProof","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"pubkey","type":"bytes","internalType":"bytes"},{"name":"strikesData","type":"uint256[]","internalType":"uint256[]"},{"name":"proof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"event","name":"StrikesDataUpdated","inputs":[{"name":"treeRoot","type":"bytes32","indexed":false,"internalType":"bytes32"},{"name":"treeCid","type":"string","indexed":false,"internalType":"string"}],"anonymous":false},{"type":"event","name":"StrikesDataWiped","inputs":[],"anonymous":false},{"type":"error","name":"InvalidProof","inputs":[]},{"type":"error","name":"InvalidReportData","inputs":[]},{"type":"error","name":"NotOracle","inputs":[]},{"type":"error","name":"ZeroBadPerformancePenaltyAmount","inputs":[]},{"type":"error","name":"ZeroEjectionFeeAmount","inputs":[]},{"type":"error","name":"ZeroModuleAddress","inputs":[]},{"type":"error","name":"ZeroOracleAddress","inputs":[]}] diff --git a/src/providers/execution/contracts/cs_fee_oracle.py b/src/providers/execution/contracts/cs_fee_oracle.py index 05d78d687..e61d681e9 100644 --- a/src/providers/execution/contracts/cs_fee_oracle.py +++ b/src/providers/execution/contracts/cs_fee_oracle.py @@ -1,5 +1,7 @@ import logging +from eth_typing import ChecksumAddress +from web3 import Web3 from web3.types import BlockIdentifier from src.providers.execution.contracts.base_oracle import BaseOracleContract @@ -35,3 +37,16 @@ def perf_leeway_bp(self, block_identifier: BlockIdentifier = "latest") -> int: } ) return resp + + def strikes(self, block_identifier: BlockIdentifier = "latest") -> ChecksumAddress: + """Return the address of the CSStrikes contract""" + + resp = self.functions.strikes().call(block_identifier=block_identifier) + logger.info( + { + "msg": "Call `strikes()`.", + "value": resp, + "block_identifier": repr(block_identifier), + } + ) + return Web3.to_checksum_address(resp) diff --git a/src/providers/execution/contracts/cs_strikes.py b/src/providers/execution/contracts/cs_strikes.py new file mode 100644 index 000000000..05b5ac81d --- /dev/null +++ b/src/providers/execution/contracts/cs_strikes.py @@ -0,0 +1,38 @@ +import logging + +from hexbytes import HexBytes +from web3.types import BlockIdentifier + +from ..base_interface import ContractInterface + +logger = logging.getLogger(__name__) + + +class CSStrikesContract(ContractInterface): + abi_path = "./assets/CSStrikes.json" + + def tree_root(self, block_identifier: BlockIdentifier = "latest") -> HexBytes: + """Root of the latest published Merkle tree""" + + resp = self.functions.treeRoot().call(block_identifier=block_identifier) + logger.info( + { + "msg": "Call `treeRoot()`.", + "value": resp, + "block_identifier": repr(block_identifier), + } + ) + return HexBytes(resp) + + def tree_cid(self, block_identifier: BlockIdentifier = "latest") -> str: + """CID of the latest published Merkle tree""" + + resp = self.functions.treeCid().call(block_identifier=block_identifier) + logger.info( + { + "msg": "Call `treeCid()`.", + "value": resp, + "block_identifier": repr(block_identifier), + } + ) + return resp diff --git a/src/web3py/extensions/csm.py b/src/web3py/extensions/csm.py index 2f80a2b39..ce6a562ee 100644 --- a/src/web3py/extensions/csm.py +++ b/src/web3py/extensions/csm.py @@ -20,6 +20,7 @@ from src.providers.execution.contracts.cs_fee_oracle import CSFeeOracleContract from src.providers.execution.contracts.cs_module import CSModuleContract from src.providers.execution.contracts.cs_parameters_registry import CSParametersRegistryContract +from src.providers.execution.contracts.cs_strikes import CSStrikesContract from src.providers.ipfs import CID, CIDv0, CIDv1, is_cid_v0 from src.types import BlockStamp, SlotNumber from src.utils.events import get_events_in_range @@ -34,6 +35,7 @@ class CSM(Module): oracle: CSFeeOracleContract accounting: CSAccountingContract fee_distributor: CSFeeDistributorContract + strikes: CSStrikesContract module: CSModuleContract params: CSParametersRegistryContract @@ -126,6 +128,15 @@ def _load_contracts(self) -> None: decode_tuples=True, ), ) + + self.strikes = cast( + CSStrikesContract, + self.w3.eth.contract( + address=self.oracle.strikes(), + ContractFactoryClass=CSFeeOracleContract, + decode_tuples=True, + ), + ) except Web3Exception as ex: logger.error({"msg": "Some of the contracts aren't healthy", "error": str(ex)}) sleep(60) From 6e5102a6a445ed93b008e8349955707494f09778 Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Fri, 21 Feb 2025 13:09:24 +0100 Subject: [PATCH 047/162] refactor: rewards_tree instead of just tree --- src/modules/csm/csm.py | 10 +++++----- src/web3py/extensions/csm.py | 4 ++-- tests/modules/csm/test_csm_module.py | 26 ++++++++++++++------------ 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index be6f22b04..fe318046d 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -94,8 +94,8 @@ def execute_module(self, last_finalized_blockstamp: BlockStamp) -> ModuleExecute def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: self.validate_state(blockstamp) - prev_root = self.w3.csm.get_csm_tree_root(blockstamp) - prev_cid = self.w3.csm.get_csm_tree_cid(blockstamp) + prev_root = self.w3.csm.get_rewards_tree_root(blockstamp) + prev_cid = self.w3.csm.get_rewards_tree_cid(blockstamp) if (prev_cid is None) != (prev_root == ZERO_HASH): raise InconsistentData(f"Got inconsistent previous tree data: {prev_root=} {prev_cid=}") @@ -125,7 +125,7 @@ def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: else: logger.info({"msg": "No previous distribution. Nothing to accumulate"}) - tree = self.make_tree(total_rewards) + tree = self.make_rewards_tree(total_rewards) tree_cid = self.publish_tree(tree) return ReportData( @@ -393,7 +393,7 @@ def get_stuck_operators(self, frame: Frame, frame_blockstamp: ReferenceBlockStam ) return set(stuck_from_digests) | set(stuck_from_events) - def make_tree(self, shares: dict[NodeOperatorId, Shares]) -> RewardsTree: + def make_rewards_tree(self, shares: dict[NodeOperatorId, Shares]) -> RewardsTree: if not shares: raise ValueError("No shares to build a tree") @@ -408,7 +408,7 @@ def make_tree(self, shares: dict[NodeOperatorId, Shares]) -> RewardsTree: shares.pop(stone) tree = RewardsTree.new(tuple((no_id, amount) for (no_id, amount) in shares.items())) - logger.info({"msg": "New tree built for the report", "root": repr(tree.root)}) + logger.info({"msg": "New rewards tree built for the report", "root": repr(tree.root)}) return tree def publish_tree(self, tree: Tree) -> CID: diff --git a/src/web3py/extensions/csm.py b/src/web3py/extensions/csm.py index ce6a562ee..e5b45a76f 100644 --- a/src/web3py/extensions/csm.py +++ b/src/web3py/extensions/csm.py @@ -48,10 +48,10 @@ def get_csm_last_processing_ref_slot(self, blockstamp: BlockStamp) -> SlotNumber FRAME_PREV_REPORT_REF_SLOT.labels("csm_oracle").set(result) return result - def get_csm_tree_root(self, blockstamp: BlockStamp) -> HexBytes: + def get_rewards_tree_root(self, blockstamp: BlockStamp) -> HexBytes: return self.fee_distributor.tree_root(blockstamp.block_hash) - def get_csm_tree_cid(self, blockstamp: BlockStamp) -> CID | None: + def get_rewards_tree_cid(self, blockstamp: BlockStamp) -> CID | None: result = self.fee_distributor.tree_cid(blockstamp.block_hash) if result == "": return None diff --git a/tests/modules/csm/test_csm_module.py b/tests/modules/csm/test_csm_module.py index 4dcf5fa9b..af50dc0cd 100644 --- a/tests/modules/csm/test_csm_module.py +++ b/tests/modules/csm/test_csm_module.py @@ -448,7 +448,7 @@ class BuildReportTestParam: curr_tree_root: HexBytes curr_tree_cid: CID | Literal[""] curr_log_cid: CID - expected_make_tree_call_args: tuple | None + expected_make_rewards_tree_call_args: tuple | None expected_func_result: tuple @@ -473,7 +473,7 @@ class BuildReportTestParam: curr_tree_root=HexBytes(ZERO_HASH), curr_tree_cid="", curr_log_cid=CID("QmLOG"), - expected_make_tree_call_args=None, + expected_make_rewards_tree_call_args=None, expected_func_result=(1, 100500, HexBytes(ZERO_HASH), "", CID("QmLOG"), 0), ), id="empty_prev_report_and_no_new_distribution", @@ -496,7 +496,9 @@ class BuildReportTestParam: curr_tree_root=HexBytes("NEW_TREE_ROOT".encode()), curr_tree_cid=CID("QmNEW_TREE"), curr_log_cid=CID("QmLOG"), - expected_make_tree_call_args=(({NodeOperatorId(0): 1, NodeOperatorId(1): 2, NodeOperatorId(2): 3},),), + expected_make_rewards_tree_call_args=( + ({NodeOperatorId(0): 1, NodeOperatorId(1): 2, NodeOperatorId(2): 3},), + ), expected_func_result=( 1, 100500, @@ -526,7 +528,7 @@ class BuildReportTestParam: curr_tree_root=HexBytes("NEW_TREE_ROOT".encode()), curr_tree_cid=CID("QmNEW_TREE"), curr_log_cid=CID("QmLOG"), - expected_make_tree_call_args=( + expected_make_rewards_tree_call_args=( ({NodeOperatorId(0): 101, NodeOperatorId(1): 202, NodeOperatorId(2): 300, NodeOperatorId(3): 3},), ), expected_func_result=( @@ -558,7 +560,7 @@ class BuildReportTestParam: curr_tree_root=HexBytes(32), curr_tree_cid="", curr_log_cid=CID("QmLOG"), - expected_make_tree_call_args=None, + expected_make_rewards_tree_call_args=None, expected_func_result=( 1, 100500, @@ -576,18 +578,18 @@ def test_build_report(csm: CSM, module: CSOracle, param: BuildReportTestParam): module.validate_state = Mock() module.report_contract.get_consensus_version = Mock(return_value=1) # mock previous report - module.w3.csm.get_csm_tree_root = Mock(return_value=param.prev_tree_root) - module.w3.csm.get_csm_tree_cid = Mock(return_value=param.prev_tree_cid) + module.w3.csm.get_rewards_tree_root = Mock(return_value=param.prev_tree_root) + module.w3.csm.get_rewards_tree_cid = Mock(return_value=param.prev_tree_cid) module.get_accumulated_rewards = Mock(return_value=param.prev_acc_shares) # mock current frame module.calculate_distribution = param.curr_distribution - module.make_tree = Mock(return_value=Mock(root=param.curr_tree_root)) + module.make_rewards_tree = Mock(return_value=Mock(root=param.curr_tree_root)) module.publish_tree = Mock(return_value=param.curr_tree_cid) module.publish_log = Mock(return_value=param.curr_log_cid) report = module.build_report(blockstamp=Mock(ref_slot=100500)) - assert module.make_tree.call_args == param.expected_make_tree_call_args + assert module.make_rewards_tree.call_args == param.expected_make_rewards_tree_call_args assert report == param.expected_func_result @@ -698,12 +700,12 @@ class MakeTreeTestParam: ), ], ) -def test_make_tree(module: CSOracle, param: MakeTreeTestParam): +def test_make_rewards_tree(module: CSOracle, param: MakeTreeTestParam): module.w3.csm.module.MAX_OPERATORS_COUNT = UINT64_MAX if param.expected_tree_values is ValueError: with pytest.raises(ValueError): - module.make_tree(param.shares) + module.make_rewards_tree(param.shares) else: - tree = module.make_tree(param.shares) + tree = module.make_rewards_tree(param.shares) assert tree.tree.values == param.expected_tree_values From 7c352d88c17182e60b91eb01cede44fe13c0f086 Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Fri, 21 Feb 2025 16:16:38 +0100 Subject: [PATCH 048/162] wip: extend StrikesList API --- src/modules/csm/types.py | 6 ++++-- tests/modules/csm/test_strikes.py | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/modules/csm/types.py b/src/modules/csm/types.py index 507dc31fa..cdfdfc571 100644 --- a/src/modules/csm/types.py +++ b/src/modules/csm/types.py @@ -16,9 +16,11 @@ class StrikesList(Sequence[int]): sentinel: int data: list[int] - def __init__(self, data: Iterable[int]) -> None: - self.data = list(data) + def __init__(self, data: Iterable[int] | None = None, maxlen: int | None = None) -> None: + self.data = list(data or []) self.sentinel = 0 + if maxlen: + self.resize(maxlen) def __len__(self) -> int: return len(self.data) diff --git a/tests/modules/csm/test_strikes.py b/tests/modules/csm/test_strikes.py index ec24dcfc5..104549c08 100644 --- a/tests/modules/csm/test_strikes.py +++ b/tests/modules/csm/test_strikes.py @@ -7,12 +7,25 @@ def test_create_empty(): strikes = StrikesList([]) assert not len(strikes) + strikes = StrikesList() + assert not len(strikes) + def test_create_not_empty(): strikes = StrikesList([1, 2, 3]) assert strikes == [1, 2, 3] +def test_create_maxlen_smaller_than_iterable(): + strikes = StrikesList([1, 2, 3], maxlen=5) + assert strikes == [1, 2, 3, 0, 0] + + +def test_create_maxlen_larger_than_iterable(): + strikes = StrikesList([1, 2, 3], maxlen=2) + assert strikes == [1, 2] + + def test_create_resize_to_smaller(): strikes = StrikesList([1, 2, 3]) strikes.resize(2) @@ -34,3 +47,7 @@ def test_add_element(): def test_is_list_like(): strikes = StrikesList([1, 2, 3]) assert is_list_like(strikes) + + arr = [4, 5] + arr.extend(strikes) + assert arr == [4, 5, 1, 2, 3] From f08d980022cd5499913e61e0c69a7ceb003127ea Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Tue, 25 Feb 2025 10:53:14 +0100 Subject: [PATCH 049/162] feat: remove `_get_staking_module` --- src/modules/csm/csm.py | 15 ++------------- src/modules/csm/distribution.py | 6 ++---- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index f66d3b04a..caa64f302 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -30,7 +30,7 @@ ) from src.utils.cache import global_lru_cache as lru_cache from src.utils.web3converter import Web3Converter -from src.web3py.extensions.lido_validators import NodeOperatorId, StakingModule +from src.web3py.extensions.lido_validators import NodeOperatorId from src.web3py.types import Web3 logger = logging.getLogger(__name__) @@ -57,13 +57,11 @@ class CSOracle(BaseModule, ConsensusModule): COMPATIBLE_ONCHAIN_VERSIONS = [(2, 3)] report_contract: CSFeeOracleContract - staking_module: StakingModule def __init__(self, w3: Web3): self.report_contract = w3.csm.oracle self.state = State.load() super().__init__(w3) - self.staking_module = self._get_staking_module() def refresh_contracts(self): self.report_contract = self.w3.csm.oracle # type: ignore @@ -96,7 +94,7 @@ def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: if (prev_cid is None) != (prev_root == ZERO_HASH): raise InconsistentData(f"Got inconsistent previous tree data: {prev_root=} {prev_cid=}") - distribution = Distribution(self.w3, self.staking_module, self.converter(blockstamp), self.state) + distribution = Distribution(self.w3, self.converter(blockstamp), self.state) total_distributed_rewards, total_rewards_map, total_rebate, logs = distribution.calculate(blockstamp) if total_distributed_rewards != sum(total_rewards_map.values()): @@ -308,12 +306,3 @@ def get_epochs_range_to_process(self, blockstamp: BlockStamp) -> tuple[EpochNumb def converter(self, blockstamp: BlockStamp) -> Web3Converter: return Web3Converter(self.get_chain_config(blockstamp), self.get_frame_config(blockstamp)) - - def _get_staking_module(self) -> StakingModule: - modules: list[StakingModule] = self.w3.lido_contracts.staking_router.get_staking_modules() - - for mod in modules: - if mod.staking_module_address == self.w3.csm.module.address: - return mod - - raise NoModuleFound diff --git a/src/modules/csm/distribution.py b/src/modules/csm/distribution.py index 04cc20aad..4c8998825 100644 --- a/src/modules/csm/distribution.py +++ b/src/modules/csm/distribution.py @@ -9,7 +9,7 @@ from src.types import NodeOperatorId, ReferenceBlockStamp, EpochNumber, StakingModuleAddress from src.utils.slot import get_reference_blockstamp from src.utils.web3converter import Web3Converter -from src.web3py.extensions.lido_validators import LidoValidator, ValidatorsByNodeOperator, StakingModule +from src.web3py.extensions.lido_validators import LidoValidator, ValidatorsByNodeOperator from src.web3py.types import Web3 logger = logging.getLogger(__name__) @@ -17,13 +17,11 @@ class Distribution: w3: Web3 - staking_module: StakingModule converter: Web3Converter state: State - def __init__(self, w3: Web3, staking_module: StakingModule, converter: Web3Converter, state: State): + def __init__(self, w3: Web3, converter: Web3Converter, state: State): self.w3 = w3 - self.staking_module = staking_module self.converter = converter self.state = state From f0a6ebc8223713a68f98e6e971eff55d2ea3c670 Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Fri, 21 Feb 2025 16:18:23 +0100 Subject: [PATCH 050/162] feat: deliver strikes --- src/modules/csm/csm.py | 167 ++++++++++++++++++++++++++--------- src/modules/csm/types.py | 6 +- src/web3py/extensions/csm.py | 9 ++ 3 files changed, 138 insertions(+), 44 deletions(-) diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index fe318046d..ed3a23c28 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -13,9 +13,9 @@ from src.metrics.prometheus.duration_meter import duration_meter from src.modules.csm.checkpoint import FrameCheckpointProcessor, FrameCheckpointsIterator, MinStepIsNotReached from src.modules.csm.log import FramePerfLog, OperatorFrameSummary -from src.modules.csm.state import State, Frame, AttestationsAccumulator -from src.modules.csm.tree import RewardsTree, Tree -from src.modules.csm.types import ReportData, Shares +from src.modules.csm.state import AttestationsAccumulator, Frame, State +from src.modules.csm.tree import RewardsTree, StrikesTree, Tree +from src.modules.csm.types import ReportData, RewardsTreeLeaf, Shares, StrikesList, StrikesTreeLeaf from src.modules.submodules.consensus import ConsensusModule from src.modules.submodules.oracle_module import BaseModule, ModuleExecuteDelay from src.modules.submodules.types import ZERO_HASH @@ -32,8 +32,9 @@ from src.utils.blockstamp import build_blockstamp from src.utils.cache import global_lru_cache as lru_cache from src.utils.slot import get_next_non_missed_slot, get_reference_blockstamp +from src.utils.types import hex_str_to_bytes from src.utils.web3converter import Web3Converter -from src.web3py.extensions.lido_validators import NodeOperatorId, StakingModule, ValidatorsByNodeOperator, LidoValidator +from src.web3py.extensions.lido_validators import LidoValidator, NodeOperatorId, StakingModule, ValidatorsByNodeOperator from src.web3py.types import Web3 logger = logging.getLogger(__name__) @@ -47,6 +48,9 @@ class CSMError(Exception): """Unrecoverable error in CSM module""" +type StrikesValidator = tuple[NodeOperatorId, HexBytes] + + class CSOracle(BaseModule, ConsensusModule): """ CSM performance module collects performance of CSM node operators and creates a Merkle tree of the resulting @@ -94,47 +98,73 @@ def execute_module(self, last_finalized_blockstamp: BlockStamp) -> ModuleExecute def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: self.validate_state(blockstamp) - prev_root = self.w3.csm.get_rewards_tree_root(blockstamp) - prev_cid = self.w3.csm.get_rewards_tree_cid(blockstamp) + prev_rewards_root = self.w3.csm.get_rewards_tree_root(blockstamp) + prev_rewards_cid = self.w3.csm.get_rewards_tree_cid(blockstamp) - if (prev_cid is None) != (prev_root == ZERO_HASH): - raise InconsistentData(f"Got inconsistent previous tree data: {prev_root=} {prev_cid=}") + if (prev_rewards_cid is None) != (prev_rewards_root == ZERO_HASH): + raise InconsistentData(f"Got inconsistent previous tree data: {prev_rewards_root=} {prev_rewards_cid=}") - total_distributed, total_rewards, logs = self.calculate_distribution(blockstamp) + prev_strikes_root = self.w3.csm.get_strikes_tree_root(blockstamp) + prev_strikes_cid = self.w3.csm.get_strikes_tree_cid(blockstamp) - if total_distributed != sum(total_rewards.values()): - raise InconsistentData(f"Invalid distribution: {sum(total_rewards.values())=} != {total_distributed=}") + if (prev_strikes_cid is None) != (prev_strikes_root == ZERO_HASH): + raise InconsistentData(f"Got inconsistent previous tree data: {prev_strikes_root=} {prev_strikes_cid=}") + total_distributed, total_rewards, strikes, logs = self.calculate_distribution(blockstamp) log_cid = self.publish_log(logs) - if not total_distributed and not total_rewards: - logger.info({"msg": "No rewards distributed in the current frame"}) + if not total_distributed and not strikes: + logger.info({"msg": "No state changes in the current report"}) return ReportData( self.get_consensus_version(blockstamp), blockstamp.ref_slot, - tree_root=prev_root, - tree_cid=prev_cid or "", + tree_root=prev_rewards_root, + tree_cid=prev_rewards_cid or "", log_cid=log_cid, distributed=0, + strikes_tree_root=prev_strikes_root, + strikes_tree_cid=prev_strikes_cid or "", ).as_tuple() - if prev_cid and prev_root != ZERO_HASH: - # Update cumulative amount of stETH shares for all operators. - for no_id, accumulated_rewards in self.get_accumulated_rewards(prev_cid, prev_root): - total_rewards[no_id] += accumulated_rewards + rewards_tree_root, rewards_cid = prev_rewards_root, prev_rewards_cid + strikes_tree_root, strikes_cid = prev_strikes_root, prev_strikes_cid + + if total_distributed: + if prev_rewards_cid and prev_rewards_root != ZERO_HASH: + # Update cumulative amount of stETH shares for all operators. + for no_id, accumulated_rewards in self.get_accumulated_rewards(prev_rewards_cid, prev_rewards_root): + total_rewards[no_id] += accumulated_rewards + else: + logger.info({"msg": "No previous distribution. Nothing to accumulate"}) + + rewards_tree = self.make_rewards_tree(total_rewards) + rewards_tree_root = rewards_tree.root + rewards_cid = self.publish_tree(rewards_tree) + + if strikes: + if prev_strikes_cid and prev_strikes_root != ZERO_HASH: + for no_id, pubkey, old_strikes in self.get_accumulated_strikes(prev_strikes_cid, prev_strikes_root): + validator = (no_id, pubkey) + if validator in strikes: + strikes[validator].extend(old_strikes) + # NOTE: Cleanup sequences like [0,0,0] since they don't bring any information. + # TODO: maxlen should come from strikes_params. + strikes_tree = self.make_strikes_tree({k: StrikesList(v, maxlen=12) for k, v in strikes.items() if sum(v)}) + strikes_tree_root = strikes_tree.root + strikes_cid = self.publish_tree(strikes_tree) else: - logger.info({"msg": "No previous distribution. Nothing to accumulate"}) - - tree = self.make_rewards_tree(total_rewards) - tree_cid = self.publish_tree(tree) + # No strikes (including zeroes) means no validators. + pass return ReportData( self.get_consensus_version(blockstamp), blockstamp.ref_slot, - tree_root=tree.root, - tree_cid=tree_cid, + tree_root=rewards_tree_root, + tree_cid=rewards_cid or "", log_cid=log_cid, distributed=total_distributed, + strikes_tree_root=strikes_tree_root, + strikes_tree_cid=strikes_cid or "", ).as_tuple() def is_main_data_submitted(self, blockstamp: BlockStamp) -> bool: @@ -225,13 +255,20 @@ def collect_data(self, blockstamp: BlockStamp) -> bool: return self.state.is_fulfilled def calculate_distribution( - self, blockstamp: ReferenceBlockStamp - ) -> tuple[Shares, defaultdict[NodeOperatorId, Shares], list[FramePerfLog]]: + self, + blockstamp: ReferenceBlockStamp, + ) -> tuple[ + Shares, + defaultdict[NodeOperatorId, Shares], + dict[StrikesValidator, list[int]], + list[FramePerfLog], + ]: """Computes distribution of fee shares at the given timestamp""" operators_to_validators = self.module_validators_by_node_operators(blockstamp) total_distributed = Shares(0) total_rewards = defaultdict[NodeOperatorId, Shares](Shares) + strikes: dict[StrikesValidator, list[int]] = {} logs: list[FramePerfLog] = [] for frame in self.state.frames: @@ -245,10 +282,18 @@ def calculate_distribution( total_rewards_to_distribute = self.w3.csm.fee_distributor.shares_to_distribute(frame_blockstamp.block_hash) rewards_to_distribute_in_frame = total_rewards_to_distribute - total_distributed - rewards_in_frame, log = self._calculate_distribution_in_frame( - frame, frame_blockstamp, rewards_to_distribute_in_frame, operators_to_validators + frame_threshold = self._get_performance_threshold(frame, blockstamp) + log = FramePerfLog(blockstamp, frame, frame_threshold) + + rewards_in_frame, strikes = self._calculate_distribution_in_frame( + frame, + frame_threshold, + rewards_to_distribute_in_frame, + operators_to_validators, + log, ) distributed_in_frame = sum(rewards_in_frame.values()) + logger.info({"msg": f"No rewards distributed in frame [{from_epoch};{to_epoch}]"}) total_distributed += distributed_in_frame if total_distributed > total_rewards_to_distribute: @@ -259,7 +304,10 @@ def calculate_distribution( logs.append(log) - return total_distributed, total_rewards, logs + if total_distributed != sum(total_rewards.values()): + raise InconsistentData(f"Invalid distribution: {sum(total_rewards.values())=} != {total_distributed=}") + + return total_distributed, total_rewards, strikes, logs def _get_ref_blockstamp_for_frame( self, blockstamp: ReferenceBlockStamp, frame_ref_epoch: EpochNumber @@ -275,14 +323,13 @@ def _get_ref_blockstamp_for_frame( def _calculate_distribution_in_frame( self, frame: Frame, - blockstamp: ReferenceBlockStamp, + threshold: float, rewards_to_distribute: int, - operators_to_validators: ValidatorsByNodeOperator + operators_to_validators: ValidatorsByNodeOperator, + log: FramePerfLog, ): - threshold = self._get_performance_threshold(frame, blockstamp) - log = FramePerfLog(blockstamp, frame, threshold) - participation_shares: defaultdict[NodeOperatorId, int] = defaultdict(int) + strikes: dict[StrikesValidator, list[int]] = defaultdict(list) stuck_operators = self.get_stuck_operators(frame, blockstamp) for (_, no_id), validators in operators_to_validators.items(): @@ -292,7 +339,9 @@ def _calculate_distribution_in_frame( continue for validator in validators: duty = self.state.data[frame].get(validator.index) - self.process_validator_duty(validator, duty, threshold, participation_shares, log_operator) + strike = self.process_validator_duty(validator, duty, threshold, participation_shares, log_operator) + pubkey = HexBytes(hex_str_to_bytes(validator.validator.pubkey)) + strikes[(no_id, pubkey)].insert(0, strike) rewards_distribution = self.calc_rewards_distribution_in_frame(participation_shares, rewards_to_distribute) @@ -301,7 +350,7 @@ def _calculate_distribution_in_frame( log.distributable = rewards_to_distribute - return rewards_distribution, log + return rewards_distribution, strikes def _get_performance_threshold(self, frame: Frame, blockstamp: ReferenceBlockStamp) -> float: network_perf = self.state.get_network_aggr(frame).perf @@ -315,28 +364,30 @@ def process_validator_duty( attestation_duty: AttestationsAccumulator | None, threshold: float, participation_shares: defaultdict[NodeOperatorId, int], - log_operator: OperatorFrameSummary - ): + log_operator: OperatorFrameSummary, + ) -> int: if attestation_duty is None: # It's possible that the validator is not assigned to any duty, hence it's performance # is not presented in the aggregates (e.g. exited, pending for activation etc). # TODO: check `sync_aggr` to strike (in case of bad sync performance) after validator exit - return + return 0 log_validator = log_operator.validators[validator.index] + log_validator.attestation_duty = attestation_duty if validator.validator.slashed is True: # It means that validator was active during the frame and got slashed and didn't meet the exit # epoch, so we should not count such validator for operator's share. log_validator.slashed = True - return + return 1 if attestation_duty.perf > threshold: # Count of assigned attestations used as a metrics of time # the validator was active in the current frame. participation_shares[validator.lido_id.operatorIndex] += attestation_duty.assigned + return 0 - log_validator.attestation_duty = attestation_duty + return 1 @staticmethod def calc_rewards_distribution_in_frame( @@ -356,7 +407,7 @@ def calc_rewards_distribution_in_frame( return rewards_distribution - def get_accumulated_rewards(self, cid: CID, root: HexBytes) -> Iterator[tuple[NodeOperatorId, Shares]]: + def get_accumulated_rewards(self, cid: CID, root: HexBytes) -> Iterator[RewardsTreeLeaf]: logger.info({"msg": "Fetching tree by CID from IPFS", "cid": repr(cid)}) tree = RewardsTree.decode(self.w3.ipfs.fetch(cid)) @@ -393,6 +444,18 @@ def get_stuck_operators(self, frame: Frame, frame_blockstamp: ReferenceBlockStam ) return set(stuck_from_digests) | set(stuck_from_events) + def get_accumulated_strikes(self, cid: CID, root: HexBytes) -> Iterator[StrikesTreeLeaf]: + logger.info({"msg": "Fetching tree by CID from IPFS", "cid": repr(cid)}) + tree = StrikesTree.decode(self.w3.ipfs.fetch(cid)) + + logger.info({"msg": "Restored tree from IPFS dump", "root": repr(tree.root)}) + + if tree.root != root: + raise ValueError("Unexpected tree root got from IPFS dump") + + for v in tree.tree.values: + yield v["value"] + def make_rewards_tree(self, shares: dict[NodeOperatorId, Shares]) -> RewardsTree: if not shares: raise ValueError("No shares to build a tree") @@ -411,6 +474,24 @@ def make_rewards_tree(self, shares: dict[NodeOperatorId, Shares]) -> RewardsTree logger.info({"msg": "New rewards tree built for the report", "root": repr(tree.root)}) return tree + def make_strikes_tree(self, strikes: dict[StrikesValidator, StrikesList]): + if not strikes: + raise ValueError("No strikes to build a tree") + + # XXX: We put a stone here to make sure, that even with only 1 validator in the tree, it's + # still possible to report strikes. The CSStrikes contract reverts if the proof's length + # is zero, which is the case when the tree has only one leaf. + stone = (NodeOperatorId(self.w3.csm.module.MAX_OPERATORS_COUNT), HexBytes(b"")) + strikes[stone] = StrikesList() + + # XXX: Remove the stone as soon as we have enough leafs to build a suitable tree. + if stone in strikes and len(strikes) > 2: + strikes.pop(stone) + + tree = StrikesTree.new(tuple((no_id, pubkey, strikes) for ((no_id, pubkey), strikes) in strikes.items())) + logger.info({"msg": "New strikes tree built for the report", "root": repr(tree.root)}) + return tree + def publish_tree(self, tree: Tree) -> CID: tree_cid = self.w3.ipfs.publish(tree.encode()) logger.info({"msg": "Tree dump uploaded to IPFS", "cid": repr(tree_cid)}) diff --git a/src/modules/csm/types.py b/src/modules/csm/types.py index cdfdfc571..177bc28ee 100644 --- a/src/modules/csm/types.py +++ b/src/modules/csm/types.py @@ -46,7 +46,7 @@ def push(self, item: int) -> None: Shares: TypeAlias = int type RewardsTreeLeaf = tuple[NodeOperatorId, Shares] -type StrikesTreeLeaf = tuple[NodeOperatorId, bytes, StrikesList] +type StrikesTreeLeaf = tuple[NodeOperatorId, HexBytes, StrikesList] @dataclass @@ -57,6 +57,8 @@ class ReportData: tree_cid: CID | Literal[""] log_cid: CID distributed: int + strikes_tree_root: HexBytes + strikes_tree_cid: CID | Literal[""] def as_tuple(self): # Tuple with report in correct order @@ -67,4 +69,6 @@ def as_tuple(self): str(self.tree_cid), str(self.log_cid), self.distributed, + self.strikes_tree_root, + self.strikes_tree_cid, ) diff --git a/src/web3py/extensions/csm.py b/src/web3py/extensions/csm.py index e5b45a76f..b2bad77b0 100644 --- a/src/web3py/extensions/csm.py +++ b/src/web3py/extensions/csm.py @@ -82,6 +82,15 @@ def get_operators_with_stucks_in_range( if any(e["args"]["stuckKeysCount"] > 0 for e in group): yield NodeOperatorId(no_id) + def get_strikes_tree_root(self, blockstamp: BlockStamp) -> HexBytes: + return self.strikes.tree_root(blockstamp.block_hash) + + def get_strikes_tree_cid(self, blockstamp: BlockStamp) -> CID | None: + result = self.strikes.tree_cid(blockstamp.block_hash) + if result == "": + return None + return CIDv0(result) if is_cid_v0(result) else CIDv1(result) + def _load_contracts(self) -> None: try: self.module = cast( From 62d2222d2b6125d90c75ddaa86c68dc5633ba6cf Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Fri, 21 Feb 2025 17:38:01 +0100 Subject: [PATCH 051/162] feat: query for strikes params --- src/modules/csm/csm.py | 34 ++++++++++++++----- .../execution/contracts/cs_accounting.py | 3 ++ .../contracts/cs_parameters_registry.py | 2 ++ src/web3py/extensions/csm.py | 11 ++++-- 4 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index ed3a23c28..7e7e33bf1 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -110,10 +110,10 @@ def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: if (prev_strikes_cid is None) != (prev_strikes_root == ZERO_HASH): raise InconsistentData(f"Got inconsistent previous tree data: {prev_strikes_root=} {prev_strikes_cid=}") - total_distributed, total_rewards, strikes, logs = self.calculate_distribution(blockstamp) + total_distributed, total_rewards, raw_strikes, logs = self.calculate_distribution(blockstamp) log_cid = self.publish_log(logs) - if not total_distributed and not strikes: + if not total_distributed and not raw_strikes: logger.info({"msg": "No state changes in the current report"}) return ReportData( self.get_consensus_version(blockstamp), @@ -141,15 +141,15 @@ def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: rewards_tree_root = rewards_tree.root rewards_cid = self.publish_tree(rewards_tree) - if strikes: + if raw_strikes: if prev_strikes_cid and prev_strikes_root != ZERO_HASH: for no_id, pubkey, old_strikes in self.get_accumulated_strikes(prev_strikes_cid, prev_strikes_root): validator = (no_id, pubkey) - if validator in strikes: - strikes[validator].extend(old_strikes) - # NOTE: Cleanup sequences like [0,0,0] since they don't bring any information. - # TODO: maxlen should come from strikes_params. - strikes_tree = self.make_strikes_tree({k: StrikesList(v, maxlen=12) for k, v in strikes.items() if sum(v)}) + if validator in raw_strikes: + raw_strikes[validator].extend(old_strikes) + + strikes = self._process_raw_strikes(raw_strikes, blockstamp) + strikes_tree = self.make_strikes_tree(strikes) strikes_tree_root = strikes_tree.root strikes_cid = self.publish_tree(strikes_tree) else: @@ -456,6 +456,24 @@ def get_accumulated_strikes(self, cid: CID, root: HexBytes) -> Iterator[StrikesT for v in tree.tree.values: yield v["value"] + def _process_raw_strikes( + self, + strikes: dict[StrikesValidator, list[int]], + blockstamp: BlockStamp, + ) -> dict[StrikesValidator, StrikesList]: + out: dict[StrikesValidator, StrikesList] = {} + + for (no_id, pubkey), raw_strikes in strikes.items(): + # NOTE: Cleanup sequences like [0,0,0] since they don't bring any information. + if not sum(raw_strikes): + continue + maxlen = self.w3.csm.get_strikes_params(no_id, blockstamp).lifetime + if (no_id, pubkey) in out: + raise CSMError(f"Double accounting of strikes for {no_id=}") + out[(no_id, pubkey)] = StrikesList(raw_strikes, maxlen=maxlen) + + return out + def make_rewards_tree(self, shares: dict[NodeOperatorId, Shares]) -> RewardsTree: if not shares: raise ValueError("No shares to build a tree") diff --git a/src/providers/execution/contracts/cs_accounting.py b/src/providers/execution/contracts/cs_accounting.py index 8a1000be3..d7df82f83 100644 --- a/src/providers/execution/contracts/cs_accounting.py +++ b/src/providers/execution/contracts/cs_accounting.py @@ -5,6 +5,8 @@ from web3.types import BlockIdentifier from src.types import NodeOperatorId +from src.utils.cache import global_lru_cache as lru_cache + from ..base_interface import ContractInterface logger = logging.getLogger(__name__) @@ -26,6 +28,7 @@ def fee_distributor(self, block_identifier: BlockIdentifier = "latest") -> Check ) return Web3.to_checksum_address(resp) + @lru_cache def get_bond_curve_id(self, node_operator_id: NodeOperatorId, block_identifier: BlockIdentifier = "latest") -> int: """Returns the curve ID""" diff --git a/src/providers/execution/contracts/cs_parameters_registry.py b/src/providers/execution/contracts/cs_parameters_registry.py index 742019e7c..c36d34782 100644 --- a/src/providers/execution/contracts/cs_parameters_registry.py +++ b/src/providers/execution/contracts/cs_parameters_registry.py @@ -5,6 +5,7 @@ from src.constants import TOTAL_BASIS_POINTS, UINT256_MAX from src.providers.execution.base_interface import ContractInterface +from src.utils.cache import global_lru_cache as lru_cache logger = logging.getLogger(__name__) @@ -119,6 +120,7 @@ def get_performance_leeway_data( ) return PerformanceLeeway(*resp) + @lru_cache def get_strikes_params( self, curve_id: int, diff --git a/src/web3py/extensions/csm.py b/src/web3py/extensions/csm.py index b2bad77b0..6c148c0a1 100644 --- a/src/web3py/extensions/csm.py +++ b/src/web3py/extensions/csm.py @@ -19,12 +19,12 @@ from src.providers.execution.contracts.cs_fee_distributor import CSFeeDistributorContract from src.providers.execution.contracts.cs_fee_oracle import CSFeeOracleContract from src.providers.execution.contracts.cs_module import CSModuleContract -from src.providers.execution.contracts.cs_parameters_registry import CSParametersRegistryContract +from src.providers.execution.contracts.cs_parameters_registry import CSParametersRegistryContract, StrikesParams from src.providers.execution.contracts.cs_strikes import CSStrikesContract from src.providers.ipfs import CID, CIDv0, CIDv1, is_cid_v0 -from src.types import BlockStamp, SlotNumber +from src.types import BlockStamp, NodeOperatorId, SlotNumber from src.utils.events import get_events_in_range -from src.web3py.extensions.lido_validators import NodeOperatorId +from src.utils.cache import global_lru_cache as lru_cache logger = logging.getLogger(__name__) @@ -91,6 +91,11 @@ def get_strikes_tree_cid(self, blockstamp: BlockStamp) -> CID | None: return None return CIDv0(result) if is_cid_v0(result) else CIDv1(result) + @lru_cache + def get_strikes_params(self, no_id: NodeOperatorId, blockstamp: BlockStamp) -> StrikesParams: + curve_id = self.accounting.get_bond_curve_id(no_id, blockstamp.block_hash) + return self.params.get_strikes_params(curve_id, blockstamp.block_hash) + def _load_contracts(self) -> None: try: self.module = cast( From 793cdeeb1c4274832b1347e1ce5ab1c2ab2d9862 Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Mon, 24 Feb 2025 14:04:37 +0100 Subject: [PATCH 052/162] refactor: strikes refactoring --- src/modules/csm/csm.py | 30 ++++++++++++++---------------- src/modules/csm/log.py | 1 + src/modules/csm/types.py | 4 +--- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index 7e7e33bf1..ba7f34a84 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -115,16 +115,6 @@ def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: if not total_distributed and not raw_strikes: logger.info({"msg": "No state changes in the current report"}) - return ReportData( - self.get_consensus_version(blockstamp), - blockstamp.ref_slot, - tree_root=prev_rewards_root, - tree_cid=prev_rewards_cid or "", - log_cid=log_cid, - distributed=0, - strikes_tree_root=prev_strikes_root, - strikes_tree_cid=prev_strikes_cid or "", - ).as_tuple() rewards_tree_root, rewards_cid = prev_rewards_root, prev_rewards_cid strikes_tree_root, strikes_cid = prev_strikes_root, prev_strikes_cid @@ -268,7 +258,7 @@ def calculate_distribution( total_distributed = Shares(0) total_rewards = defaultdict[NodeOperatorId, Shares](Shares) - strikes: dict[StrikesValidator, list[int]] = {} + strikes: dict[StrikesValidator, list[int]] = defaultdict(list) logs: list[FramePerfLog] = [] for frame in self.state.frames: @@ -285,11 +275,12 @@ def calculate_distribution( frame_threshold = self._get_performance_threshold(frame, blockstamp) log = FramePerfLog(blockstamp, frame, frame_threshold) - rewards_in_frame, strikes = self._calculate_distribution_in_frame( + rewards_in_frame = self._calculate_distribution_in_frame( frame, frame_threshold, rewards_to_distribute_in_frame, operators_to_validators, + strikes, log, ) distributed_in_frame = sum(rewards_in_frame.values()) @@ -326,10 +317,10 @@ def _calculate_distribution_in_frame( threshold: float, rewards_to_distribute: int, operators_to_validators: ValidatorsByNodeOperator, + strikes: defaultdict[StrikesValidator, list[int]], log: FramePerfLog, ): participation_shares: defaultdict[NodeOperatorId, int] = defaultdict(int) - strikes: dict[StrikesValidator, list[int]] = defaultdict(list) stuck_operators = self.get_stuck_operators(frame, blockstamp) for (_, no_id), validators in operators_to_validators.items(): @@ -339,9 +330,16 @@ def _calculate_distribution_in_frame( continue for validator in validators: duty = self.state.data[frame].get(validator.index) - strike = self.process_validator_duty(validator, duty, threshold, participation_shares, log_operator) + validator_strikes = self.process_validator_duty( + validator, + duty, + threshold, + participation_shares, + log_operator, + ) + log_operator.validators[validator.index].strikes = validator_strikes pubkey = HexBytes(hex_str_to_bytes(validator.validator.pubkey)) - strikes[(no_id, pubkey)].insert(0, strike) + strikes[(no_id, pubkey)].insert(0, validator_strikes) rewards_distribution = self.calc_rewards_distribution_in_frame(participation_shares, rewards_to_distribute) @@ -350,7 +348,7 @@ def _calculate_distribution_in_frame( log.distributable = rewards_to_distribute - return rewards_distribution, strikes + return rewards_distribution def _get_performance_threshold(self, frame: Frame, blockstamp: ReferenceBlockStamp) -> float: network_perf = self.state.get_network_aggr(frame).perf diff --git a/src/modules/csm/log.py b/src/modules/csm/log.py index 29ab24902..272a35fff 100644 --- a/src/modules/csm/log.py +++ b/src/modules/csm/log.py @@ -14,6 +14,7 @@ class LogJSONEncoder(json.JSONEncoder): ... class ValidatorFrameSummary: attestation_duty: AttestationsAccumulator = field(default_factory=AttestationsAccumulator) slashed: bool = False + strikes: int = 0 @dataclass diff --git a/src/modules/csm/types.py b/src/modules/csm/types.py index 177bc28ee..a87be860f 100644 --- a/src/modules/csm/types.py +++ b/src/modules/csm/types.py @@ -13,12 +13,10 @@ class StrikesList(Sequence[int]): """Deque-like structure to store strikes""" - sentinel: int data: list[int] def __init__(self, data: Iterable[int] | None = None, maxlen: int | None = None) -> None: self.data = list(data or []) - self.sentinel = 0 if maxlen: self.resize(maxlen) @@ -36,7 +34,7 @@ def __repr__(self) -> str: def resize(self, maxlen: int) -> None: """Update maximum length of the list""" - self.data = self.data[:maxlen] + [self.sentinel] * (maxlen - len(self.data)) + self.data = self.data[:maxlen] + [0] * (maxlen - len(self.data)) def push(self, item: int) -> None: """Push element at the beginning of the list discarding the last element""" From 4de655f8f7645f7d9288548282337f77b0da956c Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Mon, 24 Feb 2025 14:52:41 +0100 Subject: [PATCH 053/162] chore: fix missing condition --- src/modules/csm/csm.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index ba7f34a84..bcee39a09 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -284,7 +284,8 @@ def calculate_distribution( log, ) distributed_in_frame = sum(rewards_in_frame.values()) - logger.info({"msg": f"No rewards distributed in frame [{from_epoch};{to_epoch}]"}) + if not distributed_in_frame: + logger.info({"msg": f"No rewards distributed in frame [{from_epoch};{to_epoch}]"}) total_distributed += distributed_in_frame if total_distributed > total_rewards_to_distribute: From 2806394fbda98e8421a3859cd23fa9fdfac031fd Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Mon, 24 Feb 2025 20:17:57 +0100 Subject: [PATCH 054/162] feat: change StrikesList API --- src/modules/csm/types.py | 9 +++++---- tests/modules/csm/test_strikes.py | 6 ++++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/modules/csm/types.py b/src/modules/csm/types.py index a87be860f..29ce08fe0 100644 --- a/src/modules/csm/types.py +++ b/src/modules/csm/types.py @@ -1,6 +1,6 @@ import logging from dataclasses import dataclass -from typing import Iterable, Literal, Sequence, TypeAlias +from typing import Final, Iterable, Literal, Sequence, TypeAlias from hexbytes import HexBytes @@ -13,6 +13,8 @@ class StrikesList(Sequence[int]): """Deque-like structure to store strikes""" + SENTINEL: Final = 0 + data: list[int] def __init__(self, data: Iterable[int] | None = None, maxlen: int | None = None) -> None: @@ -34,12 +36,11 @@ def __repr__(self) -> str: def resize(self, maxlen: int) -> None: """Update maximum length of the list""" - self.data = self.data[:maxlen] + [0] * (maxlen - len(self.data)) + self.data = self.data[:maxlen] + [self.SENTINEL] * (maxlen - len(self.data)) def push(self, item: int) -> None: - """Push element at the beginning of the list discarding the last element""" + """Push element at the beginning of the list resizing the list to keep one more item""" self.data.insert(0, item) - self.data.pop(-1) Shares: TypeAlias = int diff --git a/tests/modules/csm/test_strikes.py b/tests/modules/csm/test_strikes.py index 104549c08..9b84b6cc4 100644 --- a/tests/modules/csm/test_strikes.py +++ b/tests/modules/csm/test_strikes.py @@ -38,10 +38,10 @@ def test_create_resize_to_larger(): assert strikes == [1, 2, 3, 0, 0] -def test_add_element(): +def test_push_element(): strikes = StrikesList([1, 2, 3]) strikes.push(4) - assert strikes == [4, 1, 2] + assert strikes == [4, 1, 2, 3] def test_is_list_like(): @@ -51,3 +51,5 @@ def test_is_list_like(): arr = [4, 5] arr.extend(strikes) assert arr == [4, 5, 1, 2, 3] + + assert sum(strikes) == 6 From 81fcd06f95b3f6f0a2dd4c15d9972560cb7fb696 Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Mon, 24 Feb 2025 20:18:26 +0100 Subject: [PATCH 055/162] refactor: StrikesTree decode routine changes --- src/modules/csm/tree.py | 49 +++++++++++++++++++++------------- tests/modules/csm/test_tree.py | 10 ++++++- 2 files changed, 39 insertions(+), 20 deletions(-) diff --git a/src/modules/csm/tree.py b/src/modules/csm/tree.py index 5aa099fb8..4b3179391 100644 --- a/src/modules/csm/tree.py +++ b/src/modules/csm/tree.py @@ -13,28 +13,11 @@ class TreeJSONEncoder(JSONEncoder): def default(self, o): if isinstance(o, bytes): - return f"0x{o.hex()}" + return HexBytes(o).hex() return super().default(o) -class TreeJSONDecoder(JSONDecoder): - # NOTE: object_pairs_hook is set unconditionally upon object initialisation, so it's required to - # override the __init__ method. - def __init__(self, *args, **kwargs) -> None: - super().__init__(*args, **kwargs, object_pairs_hook=self.__object_pairs_hook) - - @staticmethod - def __object_pairs_hook(items: list[tuple[str, Any]]): - def try_convert_all_hex_str_to_bytes(obj: Any): - if isinstance(obj, dict): - return {k: try_convert_all_hex_str_to_bytes(v) for (k, v) in obj.items()} - if isinstance(obj, list): - return [try_convert_all_hex_str_to_bytes(item) for item in obj] - if isinstance(obj, str) and obj.startswith("0x"): - return hex_str_to_bytes(obj) - return obj - - return {k: try_convert_all_hex_str_to_bytes(v) for k, v in items} +class TreeJSONDecoder(JSONDecoder): ... class Tree[LeafType: Iterable](ABC): @@ -52,6 +35,10 @@ def __init__(self, tree: StandardMerkleTree[LeafType]) -> None: def root(self) -> HexBytes: return HexBytes(self.tree.root) + @property + def values(self) -> list[LeafType]: + return [v["value"] for v in self.tree.values] + @classmethod def decode(cls, content: bytes) -> Self: """Restore a tree from a supported binary representation""" @@ -97,8 +84,32 @@ def default(self, o): return super().default(o) +class StrikesTreeJSONDecoder(TreeJSONDecoder): + # NOTE: object_pairs_hook is set unconditionally upon object initialisation, so it's required to + # override the __init__ method. + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs, object_pairs_hook=self.__object_pairs_hook) + + @staticmethod + def __object_pairs_hook(items: list[tuple[str, Any]]): + def try_decode_value(key: str, obj: Any): + if key != "value": + return obj + if not isinstance(obj, list) or not len(obj) == 3: + raise ValueError(f"Unexpected StrikesTreeLeaf value given {obj=}") + no_id, pubkey, strikes = obj + if not isinstance(pubkey, str) or not pubkey.startswith("0x"): + raise ValueError(f"Unexpected StrikesTreeLeaf value given {obj=}") + if not isinstance(strikes, list): + raise ValueError(f"Unexpected StrikesTreeLeaf value given {obj=}") + return no_id, HexBytes(hex_str_to_bytes(pubkey)), StrikesList(strikes) + + return {k: try_decode_value(k, v) for k, v in items} + + class StrikesTree(Tree[StrikesTreeLeaf]): encoder = StrikesTreeJSONEncoder + decoder = StrikesTreeJSONDecoder @classmethod def new(cls, values) -> Self: diff --git a/tests/modules/csm/test_tree.py b/tests/modules/csm/test_tree.py index 470b6a99d..6dd23a487 100644 --- a/tests/modules/csm/test_tree.py +++ b/tests/modules/csm/test_tree.py @@ -3,9 +3,10 @@ from typing import Iterable import pytest +from hexbytes import HexBytes from src.constants import UINT64_MAX -from src.modules.csm.tree import RewardsTree, StandardMerkleTree, StrikesTree, Tree, TreeJSONEncoder +from src.modules.csm.tree import RewardsTree, StandardMerkleTree, StrikesTree, Tree from src.modules.csm.types import RewardsTreeLeaf, StrikesList, StrikesTreeLeaf from src.types import NodeOperatorId from src.utils.types import hex_str_to_bytes @@ -78,3 +79,10 @@ def values(self) -> list[StrikesTreeLeaf]: (NodeOperatorId(2), hex_str_to_bytes("0x03"), StrikesList([1])), (NodeOperatorId(UINT64_MAX), hex_str_to_bytes("0x64"), StrikesList([1, 0, 1])), ] + + def test_decoded_types(self, tree: StrikesTree) -> None: + decoded = self.cls.decode(tree.encode()) + no_id, pk, strikes = decoded.values[0] + assert isinstance(no_id, int) + assert isinstance(pk, HexBytes) + assert isinstance(strikes, StrikesList) From 1f68cb97c2be351c74afebb1c3d67043df78a916 Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Mon, 24 Feb 2025 20:21:47 +0100 Subject: [PATCH 056/162] feat: track strikes per frame --- src/modules/csm/csm.py | 104 +++++++++++++++++++++++------------------ 1 file changed, 58 insertions(+), 46 deletions(-) diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index bcee39a09..f0a8ddafc 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -1,8 +1,9 @@ import logging from collections import defaultdict -from typing import Iterator +from typing import Iterable from hexbytes import HexBytes +from copy import deepcopy from src.constants import TOTAL_BASIS_POINTS, UINT64_MAX from src.metrics.prometheus.business import CONTRACT_ON_PAUSE @@ -15,7 +16,7 @@ from src.modules.csm.log import FramePerfLog, OperatorFrameSummary from src.modules.csm.state import AttestationsAccumulator, Frame, State from src.modules.csm.tree import RewardsTree, StrikesTree, Tree -from src.modules.csm.types import ReportData, RewardsTreeLeaf, Shares, StrikesList, StrikesTreeLeaf +from src.modules.csm.types import ReportData, RewardsTreeLeaf, Shares, StrikesList from src.modules.submodules.consensus import ConsensusModule from src.modules.submodules.oracle_module import BaseModule, ModuleExecuteDelay from src.modules.submodules.types import ZERO_HASH @@ -110,12 +111,9 @@ def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: if (prev_strikes_cid is None) != (prev_strikes_root == ZERO_HASH): raise InconsistentData(f"Got inconsistent previous tree data: {prev_strikes_root=} {prev_strikes_cid=}") - total_distributed, total_rewards, raw_strikes, logs = self.calculate_distribution(blockstamp) + total_distributed, total_rewards, strikes_per_frame, logs = self.calculate_distribution(blockstamp) log_cid = self.publish_log(logs) - if not total_distributed and not raw_strikes: - logger.info({"msg": "No state changes in the current report"}) - rewards_tree_root, rewards_cid = prev_rewards_root, prev_rewards_cid strikes_tree_root, strikes_cid = prev_strikes_root, prev_strikes_cid @@ -131,20 +129,18 @@ def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: rewards_tree_root = rewards_tree.root rewards_cid = self.publish_tree(rewards_tree) - if raw_strikes: - if prev_strikes_cid and prev_strikes_root != ZERO_HASH: - for no_id, pubkey, old_strikes in self.get_accumulated_strikes(prev_strikes_cid, prev_strikes_root): - validator = (no_id, pubkey) - if validator in raw_strikes: - raw_strikes[validator].extend(old_strikes) - - strikes = self._process_raw_strikes(raw_strikes, blockstamp) - strikes_tree = self.make_strikes_tree(strikes) - strikes_tree_root = strikes_tree.root - strikes_cid = self.publish_tree(strikes_tree) - else: - # No strikes (including zeroes) means no validators. - pass + historical_strikes = {} + if prev_strikes_cid and prev_strikes_root != ZERO_HASH: + historical_strikes = self.get_accumulated_strikes(prev_strikes_cid, prev_strikes_root) + + strikes = self._merge_strikes( + historical_strikes, + strikes_per_frame, + blockstamp, + ) + strikes_tree = self.make_strikes_tree(strikes) + strikes_tree_root = strikes_tree.root + strikes_cid = self.publish_tree(strikes_tree) return ReportData( self.get_consensus_version(blockstamp), @@ -250,7 +246,7 @@ def calculate_distribution( ) -> tuple[ Shares, defaultdict[NodeOperatorId, Shares], - dict[StrikesValidator, list[int]], + dict[Frame, dict[StrikesValidator, int]], list[FramePerfLog], ]: """Computes distribution of fee shares at the given timestamp""" @@ -258,7 +254,7 @@ def calculate_distribution( total_distributed = Shares(0) total_rewards = defaultdict[NodeOperatorId, Shares](Shares) - strikes: dict[StrikesValidator, list[int]] = defaultdict(list) + strikes_per_frame: dict[Frame, dict[StrikesValidator, int]] = {} logs: list[FramePerfLog] = [] for frame in self.state.frames: @@ -275,18 +271,21 @@ def calculate_distribution( frame_threshold = self._get_performance_threshold(frame, blockstamp) log = FramePerfLog(blockstamp, frame, frame_threshold) - rewards_in_frame = self._calculate_distribution_in_frame( + rewards_in_frame, strikes_in_frame = self._calculate_distribution_in_frame( frame, frame_threshold, rewards_to_distribute_in_frame, operators_to_validators, - strikes, log, ) distributed_in_frame = sum(rewards_in_frame.values()) if not distributed_in_frame: logger.info({"msg": f"No rewards distributed in frame [{from_epoch};{to_epoch}]"}) + strikes_per_frame[frame] = strikes_in_frame + if not strikes_in_frame: + logger.info({"msg": f"No strikes in frame [{from_epoch};{to_epoch}]"}) + total_distributed += distributed_in_frame if total_distributed > total_rewards_to_distribute: raise CSMError(f"Invalid distribution: {total_distributed=} > {total_rewards_to_distribute=}") @@ -299,7 +298,7 @@ def calculate_distribution( if total_distributed != sum(total_rewards.values()): raise InconsistentData(f"Invalid distribution: {sum(total_rewards.values())=} != {total_distributed=}") - return total_distributed, total_rewards, strikes, logs + return total_distributed, total_rewards, strikes_per_frame, logs def _get_ref_blockstamp_for_frame( self, blockstamp: ReferenceBlockStamp, frame_ref_epoch: EpochNumber @@ -318,10 +317,10 @@ def _calculate_distribution_in_frame( threshold: float, rewards_to_distribute: int, operators_to_validators: ValidatorsByNodeOperator, - strikes: defaultdict[StrikesValidator, list[int]], log: FramePerfLog, ): participation_shares: defaultdict[NodeOperatorId, int] = defaultdict(int) + strikes: dict[StrikesValidator, int] = {} stuck_operators = self.get_stuck_operators(frame, blockstamp) for (_, no_id), validators in operators_to_validators.items(): @@ -340,7 +339,8 @@ def _calculate_distribution_in_frame( ) log_operator.validators[validator.index].strikes = validator_strikes pubkey = HexBytes(hex_str_to_bytes(validator.validator.pubkey)) - strikes[(no_id, pubkey)].insert(0, validator_strikes) + if validator_strikes: + strikes[(no_id, pubkey)] = validator_strikes rewards_distribution = self.calc_rewards_distribution_in_frame(participation_shares, rewards_to_distribute) @@ -349,7 +349,7 @@ def _calculate_distribution_in_frame( log.distributable = rewards_to_distribute - return rewards_distribution + return rewards_distribution, strikes def _get_performance_threshold(self, frame: Frame, blockstamp: ReferenceBlockStamp) -> float: network_perf = self.state.get_network_aggr(frame).perf @@ -406,7 +406,7 @@ def calc_rewards_distribution_in_frame( return rewards_distribution - def get_accumulated_rewards(self, cid: CID, root: HexBytes) -> Iterator[RewardsTreeLeaf]: + def get_accumulated_rewards(self, cid: CID, root: HexBytes) -> Iterable[RewardsTreeLeaf]: logger.info({"msg": "Fetching tree by CID from IPFS", "cid": repr(cid)}) tree = RewardsTree.decode(self.w3.ipfs.fetch(cid)) @@ -415,8 +415,7 @@ def get_accumulated_rewards(self, cid: CID, root: HexBytes) -> Iterator[RewardsT if tree.root != root: raise ValueError("Unexpected tree root got from IPFS dump") - for v in tree.tree.values: - yield v["value"] + return tree.values def get_stuck_operators(self, frame: Frame, frame_blockstamp: ReferenceBlockStamp) -> set[NodeOperatorId]: l_epoch, _ = frame @@ -443,7 +442,7 @@ def get_stuck_operators(self, frame: Frame, frame_blockstamp: ReferenceBlockStam ) return set(stuck_from_digests) | set(stuck_from_events) - def get_accumulated_strikes(self, cid: CID, root: HexBytes) -> Iterator[StrikesTreeLeaf]: + def get_accumulated_strikes(self, cid: CID, root: HexBytes) -> dict[StrikesValidator, StrikesList]: logger.info({"msg": "Fetching tree by CID from IPFS", "cid": repr(cid)}) tree = StrikesTree.decode(self.w3.ipfs.fetch(cid)) @@ -452,24 +451,37 @@ def get_accumulated_strikes(self, cid: CID, root: HexBytes) -> Iterator[StrikesT if tree.root != root: raise ValueError("Unexpected tree root got from IPFS dump") - for v in tree.tree.values: - yield v["value"] + return {(no_id, pubkey): strikes for no_id, pubkey, strikes in tree.values} - def _process_raw_strikes( + def _merge_strikes( self, - strikes: dict[StrikesValidator, list[int]], - blockstamp: BlockStamp, + historical_strikes: dict[StrikesValidator, StrikesList], + strikes_per_frame: dict[Frame, dict[StrikesValidator, int]], + blockstamp: ReferenceBlockStamp, ) -> dict[StrikesValidator, StrikesList]: - out: dict[StrikesValidator, StrikesList] = {} + out = deepcopy(historical_strikes) - for (no_id, pubkey), raw_strikes in strikes.items(): - # NOTE: Cleanup sequences like [0,0,0] since they don't bring any information. - if not sum(raw_strikes): - continue - maxlen = self.w3.csm.get_strikes_params(no_id, blockstamp).lifetime - if (no_id, pubkey) in out: - raise CSMError(f"Double accounting of strikes for {no_id=}") - out[(no_id, pubkey)] = StrikesList(raw_strikes, maxlen=maxlen) + for frame in self.state.frames: + strikes_in_frame = strikes_per_frame[frame] + for key in strikes_in_frame: + if key not in out: + out[key] = StrikesList() + out[key].push(strikes_in_frame[key]) + + _, to_epoch = frame + frame_blockstamp = blockstamp + if to_epoch != blockstamp.ref_epoch: + frame_blockstamp = self._get_ref_blockstamp_for_frame(blockstamp, to_epoch) + + for key in out: + no_id, _ = key + if key not in strikes_in_frame: + out[key].push(StrikesList.SENTINEL) # Just shifting... + maxlen = self.w3.csm.get_strikes_params(no_id, frame_blockstamp).lifetime + out[key].resize(maxlen) + # NOTE: Cleanup sequences like [0,0,0] since they don't bring any information. + if not sum(out[key]): + del out[key] return out From fd18fe267411c6d4851766d4ac101c8a54b9a6b6 Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Tue, 25 Feb 2025 09:54:33 +0100 Subject: [PATCH 057/162] chore: reorder statements --- src/modules/csm/csm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index f0a8ddafc..8b00d00fe 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -338,8 +338,8 @@ def _calculate_distribution_in_frame( log_operator, ) log_operator.validators[validator.index].strikes = validator_strikes - pubkey = HexBytes(hex_str_to_bytes(validator.validator.pubkey)) if validator_strikes: + pubkey = HexBytes(hex_str_to_bytes(validator.validator.pubkey)) strikes[(no_id, pubkey)] = validator_strikes rewards_distribution = self.calc_rewards_distribution_in_frame(participation_shares, rewards_to_distribute) From e57eef82ad08bc07c89577ecbee3692a86963648 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Wed, 26 Feb 2025 10:56:28 +0100 Subject: [PATCH 058/162] fix: participation share should be rounded up --- src/modules/csm/distribution.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/modules/csm/distribution.py b/src/modules/csm/distribution.py index 4c8998825..fdee8a1b0 100644 --- a/src/modules/csm/distribution.py +++ b/src/modules/csm/distribution.py @@ -1,4 +1,5 @@ import logging +import math from collections import defaultdict from functools import lru_cache @@ -188,9 +189,19 @@ def process_validator_duties( log_validator.sync_duty = sync if performance > threshold: - # Count of assigned attestations used as a metrics of time the validator was active in the current frame. - # Reward share is a share of the operator's reward the validator should get. It can be less than 1. - participation_share = max(int(attestation.assigned * reward_share), 1) + # + # - Count of assigned attestations used as a metrics of time the validator was active in the current frame. + # - Reward share is a share of the operator's reward the validator should get, and + # it can be less than 1 due to the value from `CSParametersRegistry`. + # In case of decimal value, the reward should be rounded up in favour of the operator. + # + # Example: + # - Validator was 103 epochs active in the frame (assigned 103 attestations) + # - Reward share for this Operator's key is 0.85 + # 87.55 ≈ 88 of 103 participation shares should be counted for the operator key's reward. + # The rest 15 participation shares should be counted for the protocol's rebate. + # + participation_share = math.ceil(attestation.assigned * reward_share) participation_shares[validator.lido_id.operatorIndex] += participation_share rebate_share = attestation.assigned - participation_share return rebate_share From 9861c4f9723ea03c2f7e0efc7fdcab64ffffe8c6 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Wed, 26 Feb 2025 11:32:24 +0100 Subject: [PATCH 059/162] fix: only active during the frame validators should be accounted --- src/modules/csm/distribution.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/modules/csm/distribution.py b/src/modules/csm/distribution.py index fdee8a1b0..5f24a5541 100644 --- a/src/modules/csm/distribution.py +++ b/src/modules/csm/distribution.py @@ -9,6 +9,7 @@ from src.providers.execution.contracts.cs_parameters_registry import PerformanceCoefficients from src.types import NodeOperatorId, ReferenceBlockStamp, EpochNumber, StakingModuleAddress from src.utils.slot import get_reference_blockstamp +from src.utils.validator_state import is_active_validator from src.utils.web3converter import Web3Converter from src.web3py.extensions.lido_validators import LidoValidator, ValidatorsByNodeOperator from src.web3py.types import Web3 @@ -94,14 +95,19 @@ def _calculate_distribution_in_frame( network_perf = self._get_network_performance(frame) for (_, no_id), validators in operators_to_validators.items(): + active_validators = [v for v in validators if self.state.data[frame].attestations[v.index].assigned > 0] + if not active_validators: + logger.info({"msg": f"No active validators for {no_id=} in the frame. Skipping"}) + continue + logger.info({"msg": f"Calculating distribution for {no_id=}"}) log_operator = log.operators[no_id] curve_id = self.w3.csm.accounting.get_bond_curve_id(no_id, blockstamp.block_hash) perf_coeffs, perf_leeway, reward_share = self._get_curve_params(curve_id, blockstamp) - sorted_validators = sorted(validators, key=lambda v: v.index) - for key_number, validator in enumerate(sorted_validators): + sorted_active_validators = sorted(active_validators, key=lambda v: v.index) + for key_number, validator in enumerate(sorted_active_validators): key_threshold = max(network_perf - perf_leeway.get_for(key_number), 0) key_reward_share = reward_share.get_for(key_number) @@ -165,7 +171,6 @@ def process_validator_duties( if attestation is None: # It's possible that the validator is not assigned to any duty, hence it's performance # is not presented in the aggregates (e.g. exited, pending for activation etc). - # TODO: check `sync_aggr` to strike (in case of bad sync performance) after validator exit return 0 log_validator = log_operator.validators[validator.index] From ce5a0a8c415506858cf5cceb4fbd2aa3dbafb8d1 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Wed, 26 Feb 2025 11:35:05 +0100 Subject: [PATCH 060/162] fix: `CSM_ORACLE_MAX_CONCURRENCY` --- src/variables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/variables.py b/src/variables.py index a2b78b109..386cdd52b 100644 --- a/src/variables.py +++ b/src/variables.py @@ -28,7 +28,7 @@ CSM_MODULE_ADDRESS: Final = os.getenv('CSM_MODULE_ADDRESS') FINALIZATION_BATCH_MAX_REQUEST_COUNT: Final = int(os.getenv('FINALIZATION_BATCH_MAX_REQUEST_COUNT', 1000)) EL_REQUESTS_BATCH_SIZE: Final = int(os.getenv('EL_REQUESTS_BATCH_SIZE', 500)) -CSM_ORACLE_MAX_CONCURRENCY: Final = min(32, (os.cpu_count() or 1) + 4, int(os.getenv('CSM_ORACLE_MAX_CONCURRENCY', 2))) +CSM_ORACLE_MAX_CONCURRENCY: Final = min(32, int(os.getenv('CSM_ORACLE_MAX_CONCURRENCY', 2))) # We add some gas to the transaction to be sure that we have enough gas to execute corner cases # eg when we tried to submit a few reports in a single block From b488a6f42fdafcec19dba99a5e3bb74967217f8a Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Wed, 26 Feb 2025 11:39:23 +0100 Subject: [PATCH 061/162] revert: new line --- src/modules/csm/csm.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index caa64f302..608a159a2 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -54,6 +54,7 @@ class CSOracle(BaseModule, ConsensusModule): 2. Calculate the performance of each validator based on the attestations. 3. Calculate the share of each CSM node operator excluding underperforming validators. """ + COMPATIBLE_ONCHAIN_VERSIONS = [(2, 3)] report_contract: CSFeeOracleContract From 5db9b8b45d51d83a744e6447499eff349cd238ac Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Wed, 26 Feb 2025 11:42:03 +0100 Subject: [PATCH 062/162] refactor: `Duties.merge` --- src/modules/csm/state.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/modules/csm/state.py b/src/modules/csm/state.py index 7a1e53395..96794b2e7 100644 --- a/src/modules/csm/state.py +++ b/src/modules/csm/state.py @@ -34,7 +34,7 @@ def add_duty(self, included: bool) -> None: self.assigned += 1 self.included += 1 if included else 0 - def merge(self, other: 'DutyAccumulator') -> None: + def merge(self, other: Self) -> None: self.assigned += other.assigned self.included += other.included @@ -45,6 +45,14 @@ class Duties: proposals: defaultdict[ValidatorIndex, DutyAccumulator] syncs: defaultdict[ValidatorIndex, DutyAccumulator] + def merge(self, other: Self) -> None: + for val, duty in other.attestations.items(): + self.attestations[val].merge(duty) + for val, duty in other.proposals.items(): + self.proposals[val].merge(duty) + for val, duty in other.syncs.items(): + self.syncs[val].merge(duty) + type Frame = tuple[EpochNumber, EpochNumber] type StateData = dict[Frame, Duties] @@ -208,14 +216,7 @@ def overlaps(a: Frame, b: Frame): if overlaps(new_frame, frame_to_consume): assert frame_to_consume not in consumed consumed.append(frame_to_consume) - frame_to_consume_data = self.data[frame_to_consume] - new_frame_data = new_data[new_frame] - for val, duty in frame_to_consume_data.attestations.items(): - new_frame_data.attestations[val].merge(duty) - for val, duty in frame_to_consume_data.proposals.items(): - new_frame_data.proposals[val].merge(duty) - for val, duty in frame_to_consume_data.syncs.items(): - new_frame_data.syncs[val].merge(duty) + new_data[new_frame].merge(self.data[frame_to_consume]) for frame in self.frames: if frame in consumed: continue From 6f0eb5db1a6df5a57eb80c313d1bc5e93dce238b Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Wed, 26 Feb 2025 11:45:52 +0100 Subject: [PATCH 063/162] refactor: `ValidatorDuty.index` -> `validator_index` --- src/modules/csm/checkpoint.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/modules/csm/checkpoint.py b/src/modules/csm/checkpoint.py index a26599b80..e2960c685 100644 --- a/src/modules/csm/checkpoint.py +++ b/src/modules/csm/checkpoint.py @@ -36,7 +36,7 @@ class FrameCheckpoint: @dataclass class ValidatorDuty: - index: ValidatorIndex + validator_index: ValidatorIndex included: bool @@ -262,20 +262,20 @@ def _check_duties( for att_duty in att_committee: self.state.increment_att_duty( duty_epoch, - att_duty.index, + att_duty.validator_index, included=att_duty.included, ) for sync_committee in sync_committees.values(): for sync_duty in sync_committee: self.state.increment_sync_duty( duty_epoch, - sync_duty.index, + sync_duty.validator_index, included=sync_duty.included, ) for proposer_duty in propose_duties.values(): self.state.increment_prop_duty( duty_epoch, - proposer_duty.index, + proposer_duty.validator_index, included=proposer_duty.included ) self.state.add_processed_epoch(duty_epoch) @@ -295,8 +295,8 @@ def _prepare_att_committees(self, epoch: EpochNumber) -> AttestationCommittees: validators = [] # Order of insertion is used to track the positions in the committees. for validator in committee.validators: - validators.append(ValidatorDuty(index=validator, included=False)) - committees[(committee.slot, committee.index)] = validators + validators.append(ValidatorDuty(validator_index=validator, included=False)) + committees[(committee.slot, committee.validator_index)] = validators return committees @timeit( @@ -317,7 +317,7 @@ def _prepare_sync_committee( if missed_slot: continue duties[slot] = [ - ValidatorDuty(index=ValidatorIndex(int(validator)), included=False) + ValidatorDuty(validator_index=ValidatorIndex(int(validator)), included=False) for validator in sync_committee.validators ] @@ -357,7 +357,7 @@ def _prepare_propose_duties( proposer_duties = self.cc.get_proposer_duties(epoch, dependent_root) for duty in proposer_duties: duties[SlotNumber(int(duty.slot))] = ValidatorDuty( - index=ValidatorIndex(int(duty.validator_index)), included=False + validator_index=ValidatorIndex(int(duty.validator_index)), included=False ) return duties From 1ea099988b92d7ad0143152f41fecd63ab29d026 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Wed, 26 Feb 2025 11:52:22 +0100 Subject: [PATCH 064/162] refactor: types --- src/modules/csm/checkpoint.py | 35 +++++++++++++++----------------- src/providers/consensus/types.py | 10 ++++----- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/src/modules/csm/checkpoint.py b/src/modules/csm/checkpoint.py index e2960c685..e125abd2d 100644 --- a/src/modules/csm/checkpoint.py +++ b/src/modules/csm/checkpoint.py @@ -1,4 +1,5 @@ import logging +from collections import UserDict from concurrent.futures import ThreadPoolExecutor, as_completed from dataclasses import dataclass from itertools import batched @@ -112,14 +113,14 @@ def _is_min_step_reached(self): type AttestationCommittees = dict[tuple[SlotNumber, CommitteeIndex], list[ValidatorDuty]] -class SyncCommitteesCache(dict): +class SyncCommitteesCache(UserDict): max_size = variables.CSM_ORACLE_MAX_CONCURRENCY - def __setitem__(self, committee_network_index: int, value: SyncCommittee | None): + def __setitem__(self, sync_committee_period: int, value: SyncCommittee | None): if len(self) >= self.max_size: self.pop(min(self)) - super().__setitem__(committee_network_index, value) + super().__setitem__(sync_committee_period, value) SYNC_COMMITTEES_CACHE = SyncCommitteesCache() @@ -242,9 +243,9 @@ def _check_duties( ): logger.info({"msg": f"Processing epoch {duty_epoch}"}) - att_committees = self._prepare_att_committees(EpochNumber(duty_epoch)) - propose_duties = self._prepare_propose_duties(EpochNumber(duty_epoch), checkpoint_block_roots, checkpoint_slot) - sync_committees = self._prepare_sync_committee(EpochNumber(duty_epoch), duty_epoch_roots) + att_committees = self._prepare_att_committees(duty_epoch) + propose_duties = self._prepare_propose_duties(duty_epoch, checkpoint_block_roots, checkpoint_slot) + sync_committees = self._prepare_sync_committee(duty_epoch, duty_epoch_roots) for slot, root in [*duty_epoch_roots, *next_epoch_roots]: missed_slot = root is None if missed_slot: @@ -324,8 +325,8 @@ def _prepare_sync_committee( return duties def _get_sync_committee(self, epoch: EpochNumber) -> SyncCommittee: - sync_committee_network_index = epoch // EPOCHS_PER_SYNC_COMMITTEE_PERIOD - if cached_sync_committee := SYNC_COMMITTEES_CACHE.get(sync_committee_network_index): + sync_committee_period = epoch // EPOCHS_PER_SYNC_COMMITTEE_PERIOD + if cached_sync_committee := SYNC_COMMITTEES_CACHE.get(sync_committee_period): return cached_sync_committee from_epoch = EpochNumber(epoch - epoch % EPOCHS_PER_SYNC_COMMITTEE_PERIOD) to_epoch = EpochNumber(from_epoch + EPOCHS_PER_SYNC_COMMITTEE_PERIOD - 1) @@ -338,7 +339,7 @@ def _get_sync_committee(self, epoch: EpochNumber) -> SyncCommittee: ) ) sync_committee = self.cc.get_sync_committee(state_blockstamp, epoch) - SYNC_COMMITTEES_CACHE[sync_committee_network_index] = sync_committee + SYNC_COMMITTEES_CACHE[sync_committee_period] = sync_committee return sync_committee @timeit( @@ -356,9 +357,7 @@ def _prepare_propose_duties( dependent_root = self._get_dependent_root_for_proposer_duties(epoch, checkpoint_block_roots, checkpoint_slot) proposer_duties = self.cc.get_proposer_duties(epoch, dependent_root) for duty in proposer_duties: - duties[SlotNumber(int(duty.slot))] = ValidatorDuty( - validator_index=ValidatorIndex(int(duty.validator_index)), included=False - ) + duties[duty.slot] = ValidatorDuty(validator_index=duty.validator_index, included=False) return duties def _get_dependent_root_for_proposer_duties( @@ -384,13 +383,11 @@ def _get_dependent_root_for_proposer_duties( break dependent_slot = SlotNumber(int(dependent_slot - 1)) except SlotOutOfRootsRange: - dependent_non_missed_slot = SlotNumber(int( - get_prev_non_missed_slot( - self.cc, - dependent_slot, - self.finalized_blockstamp.slot_number - ).message.slot) - ) + dependent_non_missed_slot = get_prev_non_missed_slot( + self.cc, + dependent_slot, + self.finalized_blockstamp.slot_number + ).message.slot dependent_root = self.cc.get_block_root(dependent_non_missed_slot).root logger.debug( { diff --git a/src/providers/consensus/types.py b/src/providers/consensus/types.py index d0ed326fb..99085e8eb 100644 --- a/src/providers/consensus/types.py +++ b/src/providers/consensus/types.py @@ -203,12 +203,12 @@ def indexed_validators(self) -> list[Validator]: @dataclass -class SyncCommittee(FromResponse): - validators: list[str] +class SyncCommittee(Nested, FromResponse): + validators: list[ValidatorIndex] @dataclass -class ProposerDuties(FromResponse): +class ProposerDuties(Nested, FromResponse): pubkey: str - validator_index: str - slot: str + validator_index: ValidatorIndex + slot: SlotNumber From 1e43756b9a434c494d5ac888e2104ff9783c78ce Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Wed, 26 Feb 2025 12:12:42 +0100 Subject: [PATCH 065/162] refactor: distribution return values --- src/modules/csm/csm.py | 21 +++++------ src/modules/csm/distribution.py | 65 ++++++++++++++++++++------------- 2 files changed, 48 insertions(+), 38 deletions(-) diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index 608a159a2..0081fc59c 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -96,15 +96,12 @@ def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: raise InconsistentData(f"Got inconsistent previous tree data: {prev_root=} {prev_cid=}") distribution = Distribution(self.w3, self.converter(blockstamp), self.state) - total_distributed_rewards, total_rewards_map, total_rebate, logs = distribution.calculate(blockstamp) + distribution.calculate(blockstamp) - if total_distributed_rewards != sum(total_rewards_map.values()): - raise InconsistentData(f"Invalid distribution: {sum(total_rewards_map.values())=} != {total_distributed_rewards=}") + logs_cid = self.publish_log(distribution.logs) - logs_cid = self.publish_log(logs) - - if not total_distributed_rewards and not total_rewards_map: - logger.info({"msg": f"No rewards distributed in the current frame. {total_rebate=}"}) + if not distribution.total_rewards and not distribution.total_rewards_map: + logger.info({"msg": f"No rewards distributed in the current frame. {distribution.total_rebate=}"}) return ReportData( self.get_consensus_version(blockstamp), blockstamp.ref_slot, @@ -112,7 +109,7 @@ def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: tree_cid=prev_cid or "", log_cid=logs_cid, distributed=0, - rebate=total_rebate, + rebate=distribution.total_rebate, strikes_tree_root=HexBytes(ZERO_HASH), strikes_tree_cid="", ).as_tuple() @@ -120,11 +117,11 @@ def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: if prev_cid and prev_root != ZERO_HASH: # Update cumulative amount of stETH shares for all operators. for no_id, accumulated_rewards in self.get_accumulated_rewards(prev_cid, prev_root): - total_rewards_map[no_id] += accumulated_rewards + distribution.total_rewards_map[no_id] += accumulated_rewards else: logger.info({"msg": "No previous distribution. Nothing to accumulate"}) - tree = self.make_tree(total_rewards_map) + tree = self.make_tree(distribution.total_rewards_map) tree_cid = self.publish_tree(tree) return ReportData( @@ -133,8 +130,8 @@ def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: tree_root=tree.root, tree_cid=tree_cid, log_cid=logs_cid, - distributed=total_distributed_rewards, - rebate=total_rebate, + distributed=distribution.total_rewards, + rebate=distribution.total_rebate, strikes_tree_root=HexBytes(ZERO_HASH), strikes_tree_cid="", ).as_tuple() diff --git a/src/modules/csm/distribution.py b/src/modules/csm/distribution.py index 5f24a5541..3beb6c887 100644 --- a/src/modules/csm/distribution.py +++ b/src/modules/csm/distribution.py @@ -7,9 +7,9 @@ from src.modules.csm.state import DutyAccumulator, Frame, State from src.modules.csm.types import Shares from src.providers.execution.contracts.cs_parameters_registry import PerformanceCoefficients +from src.providers.execution.exceptions import InconsistentData from src.types import NodeOperatorId, ReferenceBlockStamp, EpochNumber, StakingModuleAddress from src.utils.slot import get_reference_blockstamp -from src.utils.validator_state import is_active_validator from src.utils.web3converter import Web3Converter from src.web3py.extensions.lido_validators import LidoValidator, ValidatorsByNodeOperator from src.web3py.types import Web3 @@ -22,20 +22,23 @@ class Distribution: converter: Web3Converter state: State + total_rewards: Shares + total_rewards_map: defaultdict[NodeOperatorId, Shares] + total_rebate: Shares + logs: list[FramePerfLog] + def __init__(self, w3: Web3, converter: Web3Converter, state: State): self.w3 = w3 self.converter = converter self.state = state + # Distribution results + self.total_rewards = 0 + self.total_rewards_map = defaultdict[NodeOperatorId, int](int) + self.total_rebate = 0 + self.logs: list[FramePerfLog] = [] - def calculate( - self, blockstamp: ReferenceBlockStamp - ) -> tuple[Shares, defaultdict[NodeOperatorId, Shares], Shares, list[FramePerfLog]]: + def calculate(self, blockstamp: ReferenceBlockStamp) -> None: """Computes distribution of fee shares at the given timestamp""" - total_distributed_rewards = 0 - total_rewards_map = defaultdict[NodeOperatorId, int](int) - total_rebate = 0 - logs: list[FramePerfLog] = [] - for frame in self.state.frames: logger.info({"msg": f"Calculating distribution for {frame=}"}) _, to_epoch = frame @@ -43,23 +46,30 @@ def calculate( frame_module_validators = self._get_module_validators(frame_blockstamp) total_rewards_to_distribute = self.w3.csm.fee_distributor.shares_to_distribute(frame_blockstamp.block_hash) - rewards_to_distribute_in_frame = total_rewards_to_distribute - (total_distributed_rewards + total_rebate) - - rewards_in_frame, log = self._calculate_distribution_in_frame( + distributed_so_far = self.total_rewards + self.total_rebate + rewards_to_distribute_in_frame = total_rewards_to_distribute - distributed_so_far + + ( + rewards_map_in_frame, + distributed_rewards_in_frame, + rebate_to_protocol_in_frame, + frame_log + ) = self._calculate_distribution_in_frame( frame, frame_blockstamp, rewards_to_distribute_in_frame, frame_module_validators ) - total_distributed_rewards += log.distributed_rewards - total_rebate += log.rebate_to_protocol + self.total_rewards += distributed_rewards_in_frame + self.total_rebate += rebate_to_protocol_in_frame - self.validate_distribution(total_distributed_rewards, total_rebate, total_rewards_to_distribute) + self.validate_distribution(self.total_rewards, self.total_rebate, total_rewards_to_distribute) - for no_id, rewards in rewards_in_frame.items(): - total_rewards_map[no_id] += rewards + for no_id, rewards in rewards_map_in_frame.items(): + self.total_rewards_map[no_id] += rewards - logs.append(log) + self.logs.append(frame_log) - return total_distributed_rewards, total_rewards_map, total_rebate, logs + if self.total_rewards != sum(self.total_rewards_map.values()): + raise InconsistentData(f"Invalid distribution: {sum(self.total_rewards_map.values())=} != {self.total_rewards=}") def _get_frame_blockstamp(self, blockstamp: ReferenceBlockStamp, to_epoch: EpochNumber) -> ReferenceBlockStamp: if to_epoch != blockstamp.ref_epoch: @@ -85,11 +95,11 @@ def _calculate_distribution_in_frame( self, frame: Frame, blockstamp: ReferenceBlockStamp, - rewards_to_distribute: int, + rewards_to_distribute: Shares, operators_to_validators: ValidatorsByNodeOperator, - ) -> tuple[dict[NodeOperatorId, int], FramePerfLog]: + ) -> tuple[dict[NodeOperatorId, Shares], Shares, Shares, FramePerfLog]: total_rebate_share = 0 - participation_shares: defaultdict[NodeOperatorId, int] = defaultdict(int) + participation_shares: defaultdict[NodeOperatorId, Shares] = defaultdict(int) log = FramePerfLog(blockstamp, frame) network_perf = self._get_network_performance(frame) @@ -136,11 +146,14 @@ def _calculate_distribution_in_frame( for no_id, no_rewards in rewards_distribution.items(): log.operators[no_id].distributed = no_rewards + distributed_rewards = sum(rewards_distribution.values()) + rebate_to_protocol = total_rebate_share - distributed_rewards + log.distributable = rewards_to_distribute - log.distributed_rewards = sum(rewards_distribution.values()) - log.rebate_to_protocol = rewards_to_distribute - log.distributed_rewards + log.distributed_rewards = distributed_rewards + log.rebate_to_protocol = rewards_to_distribute - distributed_rewards - return rewards_distribution, log + return rewards_distribution, distributed_rewards, rebate_to_protocol, log @lru_cache() def _get_curve_params(self, curve_id: int, blockstamp: ReferenceBlockStamp): @@ -218,7 +231,7 @@ def calc_rewards_distribution_in_frame( participation_shares: dict[NodeOperatorId, int], rebate_share: int, rewards_to_distribute: int, - ) -> dict[NodeOperatorId, int]: + ) -> dict[NodeOperatorId, Shares]: rewards_distribution: dict[NodeOperatorId, int] = defaultdict(int) total_shares = rebate_share + sum(participation_shares.values()) From 94986e84e9d3f10f0f9f04e77e94fa429f951714 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Wed, 26 Feb 2025 12:34:49 +0100 Subject: [PATCH 066/162] fix: after refactoring --- src/modules/csm/checkpoint.py | 6 +++--- src/modules/csm/distribution.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/modules/csm/checkpoint.py b/src/modules/csm/checkpoint.py index e125abd2d..c3be7e275 100644 --- a/src/modules/csm/checkpoint.py +++ b/src/modules/csm/checkpoint.py @@ -295,9 +295,9 @@ def _prepare_att_committees(self, epoch: EpochNumber) -> AttestationCommittees: for committee in self.cc.get_attestation_committees(self.finalized_blockstamp, epoch): validators = [] # Order of insertion is used to track the positions in the committees. - for validator in committee.validators: - validators.append(ValidatorDuty(validator_index=validator, included=False)) - committees[(committee.slot, committee.validator_index)] = validators + for validator_index in committee.validators: + validators.append(ValidatorDuty(validator_index, included=False)) + committees[(committee.slot, committee.index)] = validators return committees @timeit( diff --git a/src/modules/csm/distribution.py b/src/modules/csm/distribution.py index 3beb6c887..45b873b11 100644 --- a/src/modules/csm/distribution.py +++ b/src/modules/csm/distribution.py @@ -147,11 +147,11 @@ def _calculate_distribution_in_frame( log.operators[no_id].distributed = no_rewards distributed_rewards = sum(rewards_distribution.values()) - rebate_to_protocol = total_rebate_share - distributed_rewards + rebate_to_protocol = rewards_to_distribute - distributed_rewards log.distributable = rewards_to_distribute log.distributed_rewards = distributed_rewards - log.rebate_to_protocol = rewards_to_distribute - distributed_rewards + log.rebate_to_protocol = rebate_to_protocol return rewards_distribution, distributed_rewards, rebate_to_protocol, log From 5b77b7cc0509cb675f4c6ddcee1fbdf0be438edc Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Wed, 26 Feb 2025 12:40:41 +0100 Subject: [PATCH 067/162] refactor: `get_block_attestations_and_sync` lru cache size --- src/providers/consensus/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/providers/consensus/client.py b/src/providers/consensus/client.py index cf24d346c..36e7d9ef1 100644 --- a/src/providers/consensus/client.py +++ b/src/providers/consensus/client.py @@ -3,6 +3,7 @@ from json_stream.base import TransientStreamingJSONObject # type: ignore +from src import variables from src.metrics.logging import logging from src.metrics.prometheus.basic import CL_REQUESTS_DURATION from src.providers.consensus.types import ( @@ -119,7 +120,7 @@ def get_block_details(self, state_id: SlotNumber | BlockRoot) -> BlockDetailsRes raise ValueError("Expected mapping response from getBlockV2") return BlockDetailsResponse.from_response(**data) - @lru_cache(maxsize=256) + @lru_cache(maxsize=variables.CSM_ORACLE_MAX_CONCURRENCY * 32 * 2) # threads count * blocks * epochs to check duties def get_block_attestations_and_sync(self, state_id: SlotNumber | BlockRoot) -> tuple[list[BlockAttestation], SyncAggregate]: """Spec: https://ethereum.github.io/beacon-APIs/#/Beacon/getBlockV2""" data, _ = self._get( From 0696b5505afd34abb06c6bdbb7fa35defbb7ed74 Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Wed, 26 Feb 2025 12:51:09 +0100 Subject: [PATCH 068/162] fix: StrikesList equal to itself --- src/modules/csm/types.py | 4 +++- tests/modules/csm/test_strikes.py | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/modules/csm/types.py b/src/modules/csm/types.py index 29ce08fe0..29910f1b7 100644 --- a/src/modules/csm/types.py +++ b/src/modules/csm/types.py @@ -29,7 +29,9 @@ def __getitem__(self, index): return self.data[index] def __eq__(self, value: object, /) -> bool: - return self.data.__eq__(value) + if isinstance(value, StrikesList): + return self.data == value.data + return self.data == value def __repr__(self) -> str: return repr(self.data) diff --git a/tests/modules/csm/test_strikes.py b/tests/modules/csm/test_strikes.py index 9b84b6cc4..8f67ef6d2 100644 --- a/tests/modules/csm/test_strikes.py +++ b/tests/modules/csm/test_strikes.py @@ -16,6 +16,12 @@ def test_create_not_empty(): assert strikes == [1, 2, 3] +def test_eq(): + assert StrikesList([1, 2, 3]) == StrikesList([1, 2, 3]) + assert StrikesList([1, 2, 3]) != StrikesList([1, 2]) + assert StrikesList([1, 2, 3]) == [1, 2, 3] + + def test_create_maxlen_smaller_than_iterable(): strikes = StrikesList([1, 2, 3], maxlen=5) assert strikes == [1, 2, 3, 0, 0] From 9fced153e2d182386baeea59b8f28cea52f86f48 Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Wed, 26 Feb 2025 12:51:25 +0100 Subject: [PATCH 069/162] wip: tests for strikes --- tests/modules/csm/test_csm_module.py | 138 ++++++++++++++++++++++----- 1 file changed, 113 insertions(+), 25 deletions(-) diff --git a/tests/modules/csm/test_csm_module.py b/tests/modules/csm/test_csm_module.py index 799ffef18..9ec38324a 100644 --- a/tests/modules/csm/test_csm_module.py +++ b/tests/modules/csm/test_csm_module.py @@ -11,6 +11,7 @@ from src.modules.csm.csm import CSOracle from src.modules.csm.state import State from src.modules.csm.tree import RewardsTree, Tree +from src.modules.csm.types import StrikesList from src.modules.submodules.oracle_module import ModuleExecuteDelay from src.modules.submodules.types import ZERO_HASH, CurrentFrame from src.providers.ipfs import CID, CIDv0 @@ -579,60 +580,147 @@ def test_get_accumulated_shares_unexpected_root(module: CSOracle, tree: Tree): @dataclass(frozen=True) -class MakeTreeTestParam: +class RewardsTreeTestParam: shares: dict[NodeOperatorId, int] - expected_tree_values: tuple | Type[ValueError] + expected_tree_values: list | Type[ValueError] + + +@pytest.mark.parametrize( + "param", + [ + pytest.param(RewardsTreeTestParam(shares={}, expected_tree_values=ValueError), id="empty"), + ], +) +def test_make_rewards_tree_negative(module: CSOracle, param: RewardsTreeTestParam): + module.w3.csm.module.MAX_OPERATORS_COUNT = UINT64_MAX + + with pytest.raises(ValueError): + module.make_rewards_tree(param.shares) @pytest.mark.parametrize( "param", [ - pytest.param(MakeTreeTestParam(shares={}, expected_tree_values=ValueError), id="empty"), pytest.param( - MakeTreeTestParam( + RewardsTreeTestParam( shares={NodeOperatorId(0): 1, NodeOperatorId(1): 2, NodeOperatorId(2): 3}, - expected_tree_values=( - {'treeIndex': 4, 'value': (0, 1)}, - {'treeIndex': 2, 'value': (1, 2)}, - {'treeIndex': 3, 'value': (2, 3)}, - ), + expected_tree_values=[ + (0, 1), + (1, 2), + (2, 3), + ], ), id="normal_tree", ), pytest.param( - MakeTreeTestParam( + RewardsTreeTestParam( shares={NodeOperatorId(0): 1}, - expected_tree_values=( - {'treeIndex': 2, 'value': (0, 1)}, - {'treeIndex': 1, 'value': (18446744073709551615, 0)}, - ), + expected_tree_values=[ + (0, 1), + (UINT64_MAX, 0), + ], ), id="put_stone", ), pytest.param( - MakeTreeTestParam( + RewardsTreeTestParam( shares={ NodeOperatorId(0): 1, NodeOperatorId(1): 2, NodeOperatorId(2): 3, - NodeOperatorId(18446744073709551615): 0, + NodeOperatorId(UINT64_MAX): 0, }, - expected_tree_values=( - {'treeIndex': 4, 'value': (0, 1)}, - {'treeIndex': 2, 'value': (1, 2)}, - {'treeIndex': 3, 'value': (2, 3)}, - ), + expected_tree_values=[ + (0, 1), + (1, 2), + (2, 3), + ], + ), + id="remove_stone", + ), + ], +) +def test_make_rewards_tree(module: CSOracle, param: RewardsTreeTestParam): + module.w3.csm.module.MAX_OPERATORS_COUNT = UINT64_MAX + + tree = module.make_rewards_tree(param.shares) + assert tree.values == param.expected_tree_values + + +@dataclass(frozen=True) +class StrikesTreeTestParam: + strikes: dict[tuple[NodeOperatorId, HexBytes], StrikesList] + expected_tree_values: list | Type[ValueError] + + +@pytest.mark.parametrize( + "param", + [ + pytest.param(StrikesTreeTestParam(strikes={}, expected_tree_values=ValueError), id="empty"), + ], +) +def test_make_strikes_tree_negative(module: CSOracle, param: StrikesTreeTestParam): + module.w3.csm.module.MAX_OPERATORS_COUNT = UINT64_MAX + + with pytest.raises(ValueError): + module.make_strikes_tree(param.strikes) + + +@pytest.mark.parametrize( + "param", + [ + pytest.param(StrikesTreeTestParam(strikes={}, expected_tree_values=ValueError), id="empty"), + pytest.param( + StrikesTreeTestParam( + strikes={ + (NodeOperatorId(0), HexBytes(b"07c0")): StrikesList([1]), + (NodeOperatorId(1), HexBytes(b"07e8")): StrikesList([1, 2]), + (NodeOperatorId(2), HexBytes(b"0682")): StrikesList([1, 2, 3]), + }, + expected_tree_values=[ + (NodeOperatorId(0), HexBytes(b"07c0"), StrikesList([1])), + (NodeOperatorId(1), HexBytes(b"07e8"), StrikesList([1, 2])), + (NodeOperatorId(2), HexBytes(b"0682"), StrikesList([1, 2, 3])), + ], + ), + id="normal_tree", + ), + pytest.param( + StrikesTreeTestParam( + strikes={ + (NodeOperatorId(0), HexBytes(b"07c0")): StrikesList([1]), + }, + expected_tree_values=[ + (NodeOperatorId(0), HexBytes(b"07c0"), StrikesList([1])), + (NodeOperatorId(UINT64_MAX), HexBytes(b""), StrikesList()), + ], + ), + id="put_stone", + ), + pytest.param( + StrikesTreeTestParam( + strikes={ + (NodeOperatorId(0), HexBytes(b"07c0")): StrikesList([1]), + (NodeOperatorId(1), HexBytes(b"07e8")): StrikesList([1, 2]), + (NodeOperatorId(2), HexBytes(b"0682")): StrikesList([1, 2, 3]), + (NodeOperatorId(UINT64_MAX), HexBytes(b"")): StrikesList(), + }, + expected_tree_values=[ + (NodeOperatorId(0), HexBytes(b"07c0"), StrikesList([1])), + (NodeOperatorId(1), HexBytes(b"07e8"), StrikesList([1, 2])), + (NodeOperatorId(2), HexBytes(b"0682"), StrikesList([1, 2, 3])), + ], ), id="remove_stone", ), ], ) -def test_make_rewards_tree(module: CSOracle, param: MakeTreeTestParam): +def test_make_strikes_tree(module: CSOracle, param: StrikesTreeTestParam): module.w3.csm.module.MAX_OPERATORS_COUNT = UINT64_MAX if param.expected_tree_values is ValueError: with pytest.raises(ValueError): - module.make_rewards_tree(param.shares) + module.make_strikes_tree(param.strikes) else: - tree = module.make_rewards_tree(param.shares) - assert tree.tree.values == param.expected_tree_values + tree = module.make_strikes_tree(param.strikes) + assert tree.values == param.expected_tree_values From b4572411f507b4ef8e09bfb375dd7843da677fdd Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Wed, 26 Feb 2025 14:08:47 +0100 Subject: [PATCH 070/162] tests: remove _get_staking_module --- tests/modules/csm/test_csm_distribution.py | 5 ----- tests/modules/csm/test_csm_module.py | 5 ----- 2 files changed, 10 deletions(-) diff --git a/tests/modules/csm/test_csm_distribution.py b/tests/modules/csm/test_csm_distribution.py index 7cd69eee6..fdf2ce4ae 100644 --- a/tests/modules/csm/test_csm_distribution.py +++ b/tests/modules/csm/test_csm_distribution.py @@ -14,11 +14,6 @@ from tests.factory.no_registry import LidoValidatorFactory -@pytest.fixture(autouse=True) -def mock_get_staking_module(monkeypatch: pytest.MonkeyPatch): - monkeypatch.setattr(CSOracle, "_get_staking_module", Mock()) - - @pytest.fixture() def module(web3, csm: CSM): yield CSOracle(web3) diff --git a/tests/modules/csm/test_csm_module.py b/tests/modules/csm/test_csm_module.py index 65804703c..4e039f8f2 100644 --- a/tests/modules/csm/test_csm_module.py +++ b/tests/modules/csm/test_csm_module.py @@ -20,11 +20,6 @@ from tests.factory.configs import ChainConfigFactory, FrameConfigFactory -@pytest.fixture(autouse=True) -def mock_get_staking_module(monkeypatch: pytest.MonkeyPatch): - monkeypatch.setattr(CSOracle, "_get_staking_module", Mock()) - - @pytest.fixture(autouse=True) def mock_load_state(monkeypatch: pytest.MonkeyPatch): monkeypatch.setattr(State, "load", Mock()) From d53d59fef7e57d3fa39330affcb2d48f04bc127c Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Wed, 26 Feb 2025 15:58:33 +0100 Subject: [PATCH 071/162] chore: QoL refactoring --- src/modules/csm/csm.py | 5 ++--- src/providers/consensus/types.py | 6 ++++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index 6c96cca16..8cd3487eb 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -332,10 +332,9 @@ def _calculate_distribution_in_frame( participation_shares, log_operator, ) - log_operator.validators[validator.index].strikes = validator_strikes if validator_strikes: - pubkey = HexBytes(hex_str_to_bytes(validator.validator.pubkey)) - strikes[(no_id, pubkey)] = validator_strikes + strikes[(no_id, validator.pubkey)] = validator_strikes + log_operator.validators[validator.index].strikes = validator_strikes rewards_distribution = self.calc_rewards_distribution_in_frame(participation_shares, rewards_to_distribute) diff --git a/src/providers/consensus/types.py b/src/providers/consensus/types.py index 70e2980c3..ee9c0153c 100644 --- a/src/providers/consensus/types.py +++ b/src/providers/consensus/types.py @@ -1,12 +1,14 @@ from dataclasses import dataclass, field from typing import Protocol +from ens.ens import HexBytes from eth_typing import BlockNumber from web3.types import Timestamp from src.constants import FAR_FUTURE_EPOCH from src.types import BlockHash, BlockRoot, CommitteeIndex, EpochNumber, Gwei, SlotNumber, StateRoot, ValidatorIndex from src.utils.dataclass import FromResponse, Nested +from src.utils.types import hex_str_to_bytes @dataclass @@ -136,6 +138,10 @@ class Validator(Nested, FromResponse): balance: Gwei validator: ValidatorState + @property + def pubkey(self) -> HexBytes: + return HexBytes(hex_str_to_bytes(self.validator.pubkey)) + @dataclass class BlockDetailsResponse(Nested, FromResponse): From 9b2c5b034389f396103ef8832596e7556c27fe39 Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Wed, 26 Feb 2025 15:58:45 +0100 Subject: [PATCH 072/162] wip: fix tests --- tests/modules/csm/test_csm_module.py | 117 +++++++++++++++++++-------- 1 file changed, 85 insertions(+), 32 deletions(-) diff --git a/tests/modules/csm/test_csm_module.py b/tests/modules/csm/test_csm_module.py index 9ec38324a..c80de865a 100644 --- a/tests/modules/csm/test_csm_module.py +++ b/tests/modules/csm/test_csm_module.py @@ -8,10 +8,10 @@ from hexbytes import HexBytes from src.constants import UINT64_MAX -from src.modules.csm.csm import CSOracle +from src.modules.csm.csm import CSOracle, StrikesValidator from src.modules.csm.state import State from src.modules.csm.tree import RewardsTree, Tree -from src.modules.csm.types import StrikesList +from src.modules.csm.types import RewardsTreeLeaf, StrikesList from src.modules.submodules.oracle_module import ModuleExecuteDelay from src.modules.submodules.types import ZERO_HASH, CurrentFrame from src.providers.ipfs import CID, CIDv0 @@ -369,12 +369,17 @@ def test_collect_data_fulfilled_state( @dataclass(frozen=True) class BuildReportTestParam: - prev_tree_root: HexBytes - prev_tree_cid: CID | None - prev_acc_shares: Iterable[tuple[NodeOperatorId, int]] + prev_rewards_tree_root: HexBytes + prev_rewards_tree_cid: CID | None + prev_acc_shares: Iterable[RewardsTreeLeaf] + prev_strikes_tree_root: HexBytes + prev_strikes_tree_cid: CID | None + prev_acc_strikes: dict[StrikesValidator, StrikesList] curr_distribution: Mock - curr_tree_root: HexBytes - curr_tree_cid: CID | Literal[""] + curr_rewards_tree_root: HexBytes + curr_rewards_tree_cid: CID | Literal[""] + curr_strikes_tree_root: HexBytes + curr_strikes_tree_cid: CID | Literal[""] curr_log_cid: CID expected_make_rewards_tree_call_args: tuple | None expected_func_result: tuple @@ -385,44 +390,67 @@ class BuildReportTestParam: [ pytest.param( BuildReportTestParam( - prev_tree_root=HexBytes(ZERO_HASH), - prev_tree_cid=None, + prev_rewards_tree_root=HexBytes(ZERO_HASH), + prev_rewards_tree_cid=None, prev_acc_shares=[], + prev_strikes_tree_root=HexBytes(ZERO_HASH), + prev_strikes_tree_cid=None, + prev_acc_strikes={}, curr_distribution=Mock( return_value=( # distributed 0, # shares defaultdict(int), - # log + # strikes + defaultdict(dict), + # logs Mock(), ) ), - curr_tree_root=HexBytes(ZERO_HASH), - curr_tree_cid="", + curr_rewards_tree_root=HexBytes(ZERO_HASH), + curr_rewards_tree_cid="", + curr_strikes_tree_root=HexBytes(ZERO_HASH), + curr_strikes_tree_cid="", curr_log_cid=CID("QmLOG"), expected_make_rewards_tree_call_args=None, - expected_func_result=(1, 100500, HexBytes(ZERO_HASH), "", CID("QmLOG"), 0), + expected_func_result=( + 1, + 100500, + HexBytes(ZERO_HASH), + "", + CID("QmLOG"), + 0, + HexBytes(ZERO_HASH), + CID(""), + ), ), id="empty_prev_report_and_no_new_distribution", ), pytest.param( BuildReportTestParam( - prev_tree_root=HexBytes(ZERO_HASH), - prev_tree_cid=None, + prev_rewards_tree_root=HexBytes(ZERO_HASH), + prev_rewards_tree_cid=None, prev_acc_shares=[], + prev_strikes_tree_root=HexBytes(ZERO_HASH), + prev_strikes_tree_cid=None, + prev_acc_strikes={}, curr_distribution=Mock( return_value=( # distributed 6, # shares defaultdict(int, {NodeOperatorId(0): 1, NodeOperatorId(1): 2, NodeOperatorId(2): 3}), - # log + # strikes + defaultdict(dict), + # logs Mock(), ) ), - curr_tree_root=HexBytes("NEW_TREE_ROOT".encode()), - curr_tree_cid=CID("QmNEW_TREE"), + curr_rewards_tree_root=HexBytes("NEW_TREE_ROOT".encode()), + curr_rewards_tree_cid=CID("QmNEW_TREE"), + curr_strikes_tree_root=HexBytes(ZERO_HASH), + curr_strikes_tree_cid="", curr_log_cid=CID("QmLOG"), expected_make_rewards_tree_call_args=( ({NodeOperatorId(0): 1, NodeOperatorId(1): 2, NodeOperatorId(2): 3},), @@ -434,27 +462,36 @@ class BuildReportTestParam: CID("QmNEW_TREE"), CID("QmLOG"), 6, + HexBytes(ZERO_HASH), + CID(""), ), ), id="empty_prev_report_and_new_distribution", ), pytest.param( BuildReportTestParam( - prev_tree_root=HexBytes("OLD_TREE_ROOT".encode()), - prev_tree_cid=CID("QmOLD_TREE"), + prev_rewards_tree_root=HexBytes("OLD_TREE_ROOT".encode()), + prev_rewards_tree_cid=CID("QmOLD_TREE"), prev_acc_shares=[(NodeOperatorId(0), 100), (NodeOperatorId(1), 200), (NodeOperatorId(2), 300)], + prev_strikes_tree_root=HexBytes(ZERO_HASH), + prev_strikes_tree_cid=None, + prev_acc_strikes={}, curr_distribution=Mock( return_value=( # distributed 6, # shares defaultdict(int, {NodeOperatorId(0): 1, NodeOperatorId(1): 2, NodeOperatorId(3): 3}), - # log + # strikes + defaultdict(dict), + # logs Mock(), ) ), - curr_tree_root=HexBytes("NEW_TREE_ROOT".encode()), - curr_tree_cid=CID("QmNEW_TREE"), + curr_rewards_tree_root=HexBytes("NEW_TREE_ROOT".encode()), + curr_rewards_tree_cid=CID("QmNEW_TREE"), + curr_strikes_tree_root=HexBytes(ZERO_HASH), + curr_strikes_tree_cid="", curr_log_cid=CID("QmLOG"), expected_make_rewards_tree_call_args=( ({NodeOperatorId(0): 101, NodeOperatorId(1): 202, NodeOperatorId(2): 300, NodeOperatorId(3): 3},), @@ -466,27 +503,36 @@ class BuildReportTestParam: CID("QmNEW_TREE"), CID("QmLOG"), 6, + HexBytes(ZERO_HASH), + CID(""), ), ), id="non_empty_prev_report_and_new_distribution", ), pytest.param( BuildReportTestParam( - prev_tree_root=HexBytes("OLD_TREE_ROOT".encode()), - prev_tree_cid=CID("QmOLD_TREE"), + prev_rewards_tree_root=HexBytes("OLD_TREE_ROOT".encode()), + prev_rewards_tree_cid=CID("QmOLD_TREE"), prev_acc_shares=[(NodeOperatorId(0), 100), (NodeOperatorId(1), 200), (NodeOperatorId(2), 300)], + prev_strikes_tree_root=HexBytes(ZERO_HASH), + prev_strikes_tree_cid=None, + prev_acc_strikes={}, curr_distribution=Mock( return_value=( # distributed 0, # shares defaultdict(int), - # log + # strikes + defaultdict(dict), + # logs Mock(), ) ), - curr_tree_root=HexBytes(32), - curr_tree_cid="", + curr_rewards_tree_root=HexBytes(32), + curr_rewards_tree_cid="", + curr_strikes_tree_root=HexBytes(ZERO_HASH), + curr_strikes_tree_cid="", curr_log_cid=CID("QmLOG"), expected_make_rewards_tree_call_args=None, expected_func_result=( @@ -496,6 +542,8 @@ class BuildReportTestParam: CID("QmOLD_TREE"), CID("QmLOG"), 0, + HexBytes(ZERO_HASH), + CID(""), ), ), id="non_empty_prev_report_and_no_new_distribution", @@ -506,13 +554,18 @@ def test_build_report(csm: CSM, module: CSOracle, param: BuildReportTestParam): module.validate_state = Mock() module.report_contract.get_consensus_version = Mock(return_value=1) # mock previous report - module.w3.csm.get_rewards_tree_root = Mock(return_value=param.prev_tree_root) - module.w3.csm.get_rewards_tree_cid = Mock(return_value=param.prev_tree_cid) + module.w3.csm.get_rewards_tree_root = Mock(return_value=param.prev_rewards_tree_root) + module.w3.csm.get_rewards_tree_cid = Mock(return_value=param.prev_rewards_tree_cid) + module.w3.csm.get_strikes_tree_root = Mock(return_value=param.prev_strikes_tree_root) + module.w3.csm.get_strikes_tree_cid = Mock(return_value=param.prev_strikes_tree_cid) module.get_accumulated_rewards = Mock(return_value=param.prev_acc_shares) + module.get_accumulated_strikes = Mock(return_value=param.prev_acc_strikes) # mock current frame module.calculate_distribution = param.curr_distribution - module.make_rewards_tree = Mock(return_value=Mock(root=param.curr_tree_root)) - module.publish_tree = Mock(return_value=param.curr_tree_cid) + module._merge_strikes = Mock() + module.make_rewards_tree = Mock(return_value=Mock(root=param.curr_rewards_tree_root)) + module.make_strikes_tree = Mock(return_value=Mock(root=param.curr_strikes_tree_root)) + module.publish_tree = Mock(side_effect=[param.curr_rewards_tree_cid, param.curr_strikes_tree_cid]) module.publish_log = Mock(return_value=param.curr_log_cid) report = module.build_report(blockstamp=Mock(ref_slot=100500)) From 71ae9bbc241fcd0c0dc74f6d0f9267d9b3d975db Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Wed, 26 Feb 2025 16:08:35 +0100 Subject: [PATCH 073/162] feat: state tests --- src/modules/csm/state.py | 29 ++- tests/modules/csm/test_state.py | 436 +++++++++++++++++--------------- 2 files changed, 246 insertions(+), 219 deletions(-) diff --git a/src/modules/csm/state.py b/src/modules/csm/state.py index 96794b2e7..1acfbec03 100644 --- a/src/modules/csm/state.py +++ b/src/modules/csm/state.py @@ -2,7 +2,7 @@ import os import pickle from collections import defaultdict -from dataclasses import dataclass +from dataclasses import dataclass, field from functools import lru_cache from itertools import batched from pathlib import Path @@ -41,9 +41,9 @@ def merge(self, other: Self) -> None: @dataclass class Duties: - attestations: defaultdict[ValidatorIndex, DutyAccumulator] - proposals: defaultdict[ValidatorIndex, DutyAccumulator] - syncs: defaultdict[ValidatorIndex, DutyAccumulator] + attestations: defaultdict[ValidatorIndex, DutyAccumulator] = field(default_factory=lambda: defaultdict(DutyAccumulator)) + proposals: defaultdict[ValidatorIndex, DutyAccumulator] = field(default_factory=lambda: defaultdict(DutyAccumulator)) + syncs: defaultdict[ValidatorIndex, DutyAccumulator] = field(default_factory=lambda: defaultdict(DutyAccumulator)) def merge(self, other: Self) -> None: for val, duty in other.attestations.items(): @@ -201,11 +201,7 @@ def _migrate_frames_data(self, new_frames: list[Frame]): logger.info({"msg": f"Migrating duties data cache: {self.frames=} -> {new_frames=}"}) new_data: StateData = {} for frame in new_frames: - new_data[frame] = Duties( - attestations=defaultdict(DutyAccumulator), - proposals=defaultdict(DutyAccumulator), - syncs=defaultdict(DutyAccumulator), - ) + new_data[frame] = Duties() def overlaps(a: Frame, b: Frame): return a[0] <= b[0] and a[1] >= b[1] @@ -238,17 +234,26 @@ def validate(self, l_epoch: EpochNumber, r_epoch: EpochNumber) -> None: def get_att_network_aggr(self, frame: Frame) -> DutyAccumulator: # TODO: exclude `active_slashed` validators from the calculation - aggr = self.get_duty_network_aggr(self.data[frame].attestations) + frame_data = self.data.get(frame) + if frame_data is None: + raise ValueError(f"No data for frame: {frame=}") + aggr = self.get_duty_network_aggr(frame_data.attestations) logger.info({"msg": "Network attestations aggregate computed", "value": repr(aggr), "avg_perf": aggr.perf}) return aggr def get_prop_network_aggr(self, frame: Frame) -> DutyAccumulator: - aggr = self.get_duty_network_aggr(self.data[frame].proposals) + frame_data = self.data.get(frame) + if frame_data is None: + raise ValueError(f"No data for frame: {frame=}") + aggr = self.get_duty_network_aggr(frame_data.proposals) logger.info({"msg": "Network proposal aggregate computed", "value": repr(aggr), "avg_perf": aggr.perf}) return aggr def get_sync_network_aggr(self, frame: Frame) -> DutyAccumulator: - aggr = self.get_duty_network_aggr(self.data[frame].syncs) + frame_data = self.data.get(frame) + if frame_data is None: + raise ValueError(f"No data for frame: {frame=}") + aggr = self.get_duty_network_aggr(frame_data.syncs) logger.info({"msg": "Network syncs aggregate computed", "value": repr(aggr), "avg_perf": aggr.perf}) return aggr diff --git a/tests/modules/csm/test_state.py b/tests/modules/csm/test_state.py index de91a7227..10a01d776 100644 --- a/tests/modules/csm/test_state.py +++ b/tests/modules/csm/test_state.py @@ -7,7 +7,7 @@ import pytest from src import variables -from src.modules.csm.state import State, InvalidState, DutyAccumulator +from src.modules.csm.state import State, InvalidState, DutyAccumulator, Duties from src.types import ValidatorIndex from src.utils.range import sequence @@ -80,9 +80,7 @@ def test_is_empty_returns_true_for_empty_state(): def test_is_empty_returns_false_for_non_empty_state(): state = State() - state.att_data = {(0, 31): defaultdict(DutyAccumulator)} - state.prop_data = {(0, 31): defaultdict(DutyAccumulator)} - state.sync_data = {(0, 31): defaultdict(DutyAccumulator)} + state.data = {(0, 31): Duties()} assert not state.is_empty @@ -150,12 +148,12 @@ def test_increment_att_duty_adds_duty_correctly(): frame = (0, 31) state.frames = [frame] duty_epoch, _ = frame - state.att_data = { - frame: defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), + state.data = { + frame: Duties(attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})), } state.increment_att_duty(duty_epoch, ValidatorIndex(1), True) - assert state.att_data[frame][ValidatorIndex(1)].assigned == 11 - assert state.att_data[frame][ValidatorIndex(1)].included == 6 + assert state.data[frame].attestations[ValidatorIndex(1)].assigned == 11 + assert state.data[frame].attestations[ValidatorIndex(1)].included == 6 def test_increment_prop_duty_adds_duty_correctly(): @@ -163,12 +161,12 @@ def test_increment_prop_duty_adds_duty_correctly(): frame = (0, 31) state.frames = [frame] duty_epoch, _ = frame - state.prop_data = { - frame: defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), + state.data = { + frame: Duties(proposals=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})), } state.increment_prop_duty(duty_epoch, ValidatorIndex(1), True) - assert state.prop_data[frame][ValidatorIndex(1)].assigned == 11 - assert state.prop_data[frame][ValidatorIndex(1)].included == 6 + assert state.data[frame].proposals[ValidatorIndex(1)].assigned == 11 + assert state.data[frame].proposals[ValidatorIndex(1)].included == 6 def test_increment_sync_duty_adds_duty_correctly(): @@ -176,12 +174,12 @@ def test_increment_sync_duty_adds_duty_correctly(): frame = (0, 31) state.frames = [frame] duty_epoch, _ = frame - state.sync_data = { - frame: defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), + state.data = { + frame: Duties(syncs=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})), } state.increment_sync_duty(duty_epoch, ValidatorIndex(1), True) - assert state.sync_data[frame][ValidatorIndex(1)].assigned == 11 - assert state.sync_data[frame][ValidatorIndex(1)].included == 6 + assert state.data[frame].syncs[ValidatorIndex(1)].assigned == 11 + assert state.data[frame].syncs[ValidatorIndex(1)].included == 6 def test_increment_att_duty_creates_new_validator_entry(): @@ -189,12 +187,12 @@ def test_increment_att_duty_creates_new_validator_entry(): frame = (0, 31) state.frames = [frame] duty_epoch, _ = frame - state.att_data = { - frame: defaultdict(DutyAccumulator), + state.data = { + frame: Duties(), } state.increment_att_duty(duty_epoch, ValidatorIndex(2), True) - assert state.att_data[frame][ValidatorIndex(2)].assigned == 1 - assert state.att_data[frame][ValidatorIndex(2)].included == 1 + assert state.data[frame].attestations[ValidatorIndex(2)].assigned == 1 + assert state.data[frame].attestations[ValidatorIndex(2)].included == 1 def test_increment_prop_duty_creates_new_validator_entry(): @@ -202,12 +200,12 @@ def test_increment_prop_duty_creates_new_validator_entry(): frame = (0, 31) state.frames = [frame] duty_epoch, _ = frame - state.prop_data = { - frame: defaultdict(DutyAccumulator), + state.data = { + frame: Duties(), } state.increment_prop_duty(duty_epoch, ValidatorIndex(2), True) - assert state.prop_data[frame][ValidatorIndex(2)].assigned == 1 - assert state.prop_data[frame][ValidatorIndex(2)].included == 1 + assert state.data[frame].proposals[ValidatorIndex(2)].assigned == 1 + assert state.data[frame].proposals[ValidatorIndex(2)].included == 1 def test_increment_sync_duty_creates_new_validator_entry(): @@ -215,12 +213,12 @@ def test_increment_sync_duty_creates_new_validator_entry(): frame = (0, 31) state.frames = [frame] duty_epoch, _ = frame - state.sync_data = { - frame: defaultdict(DutyAccumulator), + state.data = { + frame: Duties(), } state.increment_sync_duty(duty_epoch, ValidatorIndex(2), True) - assert state.sync_data[frame][ValidatorIndex(2)].assigned == 1 - assert state.sync_data[frame][ValidatorIndex(2)].included == 1 + assert state.data[frame].syncs[ValidatorIndex(2)].assigned == 1 + assert state.data[frame].syncs[ValidatorIndex(2)].included == 1 def test_increment_att_duty_handles_non_included_duty(): @@ -228,12 +226,12 @@ def test_increment_att_duty_handles_non_included_duty(): frame = (0, 31) state.frames = [frame] duty_epoch, _ = frame - state.att_data = { - frame: defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), + state.data = { + frame: Duties(attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})), } state.increment_att_duty(duty_epoch, ValidatorIndex(1), False) - assert state.att_data[frame][ValidatorIndex(1)].assigned == 11 - assert state.att_data[frame][ValidatorIndex(1)].included == 5 + assert state.data[frame].attestations[ValidatorIndex(1)].assigned == 11 + assert state.data[frame].attestations[ValidatorIndex(1)].included == 5 def test_increment_prop_duty_handles_non_included_duty(): @@ -241,12 +239,12 @@ def test_increment_prop_duty_handles_non_included_duty(): frame = (0, 31) state.frames = [frame] duty_epoch, _ = frame - state.prop_data = { - frame: defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), + state.data = { + frame: Duties(proposals=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})), } state.increment_prop_duty(duty_epoch, ValidatorIndex(1), False) - assert state.prop_data[frame][ValidatorIndex(1)].assigned == 11 - assert state.prop_data[frame][ValidatorIndex(1)].included == 5 + assert state.data[frame].proposals[ValidatorIndex(1)].assigned == 11 + assert state.data[frame].proposals[ValidatorIndex(1)].included == 5 def test_increment_sync_duty_handles_non_included_duty(): @@ -254,12 +252,12 @@ def test_increment_sync_duty_handles_non_included_duty(): frame = (0, 31) state.frames = [frame] duty_epoch, _ = frame - state.sync_data = { - frame: defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), + state.data = { + frame: Duties(syncs=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})), } state.increment_sync_duty(duty_epoch, ValidatorIndex(1), False) - assert state.sync_data[frame][ValidatorIndex(1)].assigned == 11 - assert state.sync_data[frame][ValidatorIndex(1)].included == 5 + assert state.data[frame].syncs[ValidatorIndex(1)].assigned == 11 + assert state.data[frame].syncs[ValidatorIndex(1)].included == 5 def test_increment_att_duty_raises_error_for_out_of_range_epoch(): @@ -308,17 +306,21 @@ def test_add_processed_epoch_does_not_duplicate_epochs(): assert len(state._processed_epochs) == 1 -def test_init_or_migrate_discards_data_on_version_change(): +def test_migrate_discards_data_on_version_change(): state = State() state._consensus_version = 1 state.clear = Mock() state.commit = Mock() state.migrate(0, 63, 32, 2) + + assert state.frames == [(0, 31), (32, 63)] + assert state._epochs_to_process == tuple(sequence(0, 63)) + assert state._consensus_version == 2 state.clear.assert_called_once() state.commit.assert_called_once() -def test_init_or_migrate_no_migration_needed(): +def test_migrate_no_migration_needed(): state = State() state._consensus_version = 1 state.frames = [(0, 31), (32, 63)] @@ -326,104 +328,113 @@ def test_init_or_migrate_no_migration_needed(): (0, 31): defaultdict(DutyAccumulator), (32, 63): defaultdict(DutyAccumulator), } + state._epochs_to_process = tuple(sequence(0, 63)) state.commit = Mock() state.migrate(0, 63, 32, 1) + + assert state.frames == [(0, 31), (32, 63)] + assert state._epochs_to_process == tuple(sequence(0, 63)) + assert state._consensus_version == 1 state.commit.assert_not_called() -def test_init_or_migrate_migrates_data(): +def test_migrate_migrates_data(): state = State() state._consensus_version = 1 state.frames = [(0, 31), (32, 63)] - state.att_data = { - (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), - (32, 63): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(20, 15)}), - } - state.prop_data = { - (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), - (32, 63): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(20, 15)}), - } - state.sync_data = { - (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), - (32, 63): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(20, 15)}), + state.data = { + (0, 31): Duties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), + ), + (32, 63): Duties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(20, 15)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(20, 15)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(20, 15)}), + ), } state.commit = Mock() state.migrate(0, 63, 64, 1) - assert state.att_data == { - (0, 63): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(30, 20)}), - } - assert state.prop_data == { - (0, 63): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(30, 20)}), - } - assert state.sync_data == { - (0, 63): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(30, 20)}), + + assert state.data == { + (0, 63): Duties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(30, 20)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(30, 20)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(30, 20)}), + ), } + assert state.frames == [(0, 63)] + assert state._epochs_to_process == tuple(sequence(0, 63)) + assert state._consensus_version == 1 state.commit.assert_called_once() -def test_init_or_migrate_invalidates_unmigrated_frames(): +def test_migrate_invalidates_unmigrated_frames(): state = State() state._consensus_version = 1 state.frames = [(0, 63)] - state.att_data = { - (0, 63): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(30, 20)}), - } - state.prop_data = { - (0, 63): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(30, 20)}), - } - state.sync_data = { - (0, 63): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(30, 20)}), + state.data = { + (0, 63): Duties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(30, 20)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(30, 20)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(30, 20)}), + ), } state.commit = Mock() state.migrate(0, 31, 32, 1) - assert state.att_data == { - (0, 31): defaultdict(DutyAccumulator), - } - assert state.prop_data == { - (0, 31): defaultdict(DutyAccumulator), - } - assert state.sync_data == { - (0, 31): defaultdict(DutyAccumulator), + + assert state.data == { + (0, 31): Duties(), } assert state._processed_epochs == set() + assert state.frames == [(0, 31)] + assert state._epochs_to_process == tuple(sequence(0, 31)) + assert state._consensus_version == 1 state.commit.assert_called_once() -def test_init_or_migrate_discards_unmigrated_frame(): +def test_migrate_discards_unmigrated_frame(): state = State() state._consensus_version = 1 state.frames = [(0, 31), (32, 63), (64, 95)] - state.att_data = { - (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), - (32, 63): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(20, 15)}), - (64, 95): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(30, 25)}), - } - state.prop_data = { - (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), - (32, 63): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(20, 15)}), - (64, 95): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(30, 25)}), - } - state.sync_data = { - (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), - (32, 63): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(20, 15)}), - (64, 95): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(30, 25)}), + state.data = { + (0, 31): Duties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), + ), + (32, 63): Duties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(20, 15)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(20, 15)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(20, 15)}), + ), + (64, 95): Duties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(30, 25)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(30, 25)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(30, 25)}), + ), } state._processed_epochs = set(sequence(0, 95)) state.commit = Mock() state.migrate(0, 63, 32, 1) - assert state.att_data == { - (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), - (32, 63): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(20, 15)}), - } - assert state.prop_data == { - (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), - (32, 63): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(20, 15)}), - } - assert state.sync_data == { - (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), - (32, 63): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(20, 15)}), + + assert state.data == { + (0, 31): Duties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), + ), + (32, 63): Duties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(20, 15)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(20, 15)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(20, 15)}), + ), } assert state._processed_epochs == set(sequence(0, 63)) + assert state.frames == [(0, 31), (32, 63)] + assert state._epochs_to_process == tuple(sequence(0, 63)) + assert state._consensus_version == 1 state.commit.assert_called_once() @@ -431,124 +442,123 @@ def test_migrate_frames_data_creates_new_data_correctly(): state = State() state.frames = [(0, 31), (32, 63)] new_frames = [(0, 63)] - state.att_data = { - (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), - (32, 63): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(20, 15)}), - } - state.prop_data = { - (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), - (32, 63): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(20, 15)}), - } - state.sync_data = { - (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), - (32, 63): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(20, 15)}), - } + state.data = { + (0, 31): Duties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), + ), + (32, 63): Duties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(20, 15)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(20, 15)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(20, 15)}), + ), + } + state._processed_epochs = set(sequence(0, 20)) + state._migrate_frames_data(new_frames) - assert state.att_data == { - (0, 63): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(30, 20)}) - } - assert state.prop_data == { - (0, 63): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(30, 20)}) - } - assert state.sync_data == { - (0, 63): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(30, 20)}) + + assert state.data == { + (0, 63): Duties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(30, 20)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(30, 20)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(30, 20)}), + ), } + assert state._processed_epochs == set(sequence(0, 20)) def test_migrate_frames_data_handles_no_migration(): state = State() state.frames = [(0, 31)] new_frames = [(0, 31)] - state.att_data = { - (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), - } - state.prop_data = { - (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), - } - state.sync_data = { - (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), + state.data = { + (0, 31): Duties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), + ), } + state._processed_epochs = set(sequence(0, 20)) + state._migrate_frames_data(new_frames) - assert state.att_data == { - (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}) - } - assert state.prop_data == { - (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}) - } - assert state.sync_data == { - (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}) + + assert state.data == { + (0, 31): Duties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), + ), } + assert state._processed_epochs == set(sequence(0, 20)) def test_migrate_frames_data_handles_partial_migration(): state = State() state.frames = [(0, 31), (32, 63)] new_frames = [(0, 31), (32, 95)] - state.att_data = { - (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), - (32, 63): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(20, 15)}), - } - state.prop_data = { - (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), - (32, 63): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(20, 15)}), - } - state.sync_data = { - (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), - (32, 63): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(20, 15)}), - } + state.data = { + (0, 31): Duties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), + ), + (32, 63): Duties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(20, 15)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(20, 15)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(20, 15)}), + ), + } + state._processed_epochs = set(sequence(0, 20)) + state._migrate_frames_data(new_frames) - assert state.att_data == { - (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), - (32, 95): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(20, 15)}), - } - assert state.prop_data == { - (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), - (32, 95): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(20, 15)}), - } - assert state.sync_data == { - (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), - (32, 95): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(20, 15)}), + + assert state.data == { + (0, 31): Duties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), + ), + (32, 95): Duties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(20, 15)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(20, 15)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(20, 15)}), + ), } + assert state._processed_epochs == set(sequence(0, 20)) + def test_migrate_frames_data_handles_no_data(): state = State() state.frames = [(0, 31)] new_frames = [(0, 31)] - state.att_data = {frame: defaultdict(DutyAccumulator) for frame in state.frames} - state.prop_data = {frame: defaultdict(DutyAccumulator) for frame in state.frames} - state.sync_data = {frame: defaultdict(DutyAccumulator) for frame in state.frames} + state.data = {frame: Duties() for frame in state.frames} + state._migrate_frames_data(new_frames) - assert state.att_data == {(0, 31): defaultdict(DutyAccumulator)} - assert state.prop_data == {(0, 31): defaultdict(DutyAccumulator)} - assert state.sync_data == {(0, 31): defaultdict(DutyAccumulator)} + + assert state.data == {(0, 31): Duties()} def test_migrate_frames_data_handles_wider_old_frame(): state = State() state.frames = [(0, 63)] new_frames = [(0, 31), (32, 63)] - state.att_data = { - (0, 63): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(30, 20)}), - } - state.prop_data = { - (0, 63): defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(30, 20)}), - } - state.sync_data = { - (0, 63): defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(30, 20)}), + state.data = { + (0, 63): Duties( + attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(30, 20)}), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(30, 20)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(30, 20)}), + ), } + state._processed_epochs = set(sequence(0, 20)) + state._migrate_frames_data(new_frames) - assert state.att_data == { - (0, 31): defaultdict(DutyAccumulator), - (32, 63): defaultdict(DutyAccumulator), - } - assert state.prop_data == { - (0, 31): defaultdict(DutyAccumulator), - (32, 63): defaultdict(DutyAccumulator), - } - assert state.sync_data == { - (0, 31): defaultdict(DutyAccumulator), - (32, 63): defaultdict(DutyAccumulator), + + assert state.data == { + (0, 31): Duties(), + (32, 63): Duties(), } + assert state._processed_epochs == set() def test_validate_raises_error_if_state_not_fulfilled(): @@ -590,10 +600,12 @@ def test_attestation_aggregate_perf(): def test_get_att_network_aggr_computes_correctly(): state = State() - state.att_data = { - (0, 31): defaultdict( - DutyAccumulator, - {ValidatorIndex(1): DutyAccumulator(10, 5), ValidatorIndex(2): DutyAccumulator(20, 15)}, + state.data = { + (0, 31): Duties( + attestations=defaultdict( + DutyAccumulator, + {ValidatorIndex(1): DutyAccumulator(10, 5), ValidatorIndex(2): DutyAccumulator(20, 15)}, + ) ) } aggr = state.get_att_network_aggr((0, 31)) @@ -603,10 +615,12 @@ def test_get_att_network_aggr_computes_correctly(): def test_get_sync_network_aggr_computes_correctly(): state = State() - state.sync_data = { - (0, 31): defaultdict( - DutyAccumulator, - {ValidatorIndex(1): DutyAccumulator(10, 5), ValidatorIndex(2): DutyAccumulator(20, 15)}, + state.data = { + (0, 31): Duties( + syncs=defaultdict( + DutyAccumulator, + {ValidatorIndex(1): DutyAccumulator(10, 5), ValidatorIndex(2): DutyAccumulator(20, 15)}, + ) ) } aggr = state.get_sync_network_aggr((0, 31)) @@ -616,10 +630,12 @@ def test_get_sync_network_aggr_computes_correctly(): def test_get_prop_network_aggr_computes_correctly(): state = State() - state.prop_data = { - (0, 31): defaultdict( - DutyAccumulator, - {ValidatorIndex(1): DutyAccumulator(10, 5), ValidatorIndex(2): DutyAccumulator(20, 15)}, + state.data = { + (0, 31): Duties( + proposals=defaultdict( + DutyAccumulator, + {ValidatorIndex(1): DutyAccumulator(10, 5), ValidatorIndex(2): DutyAccumulator(20, 15)}, + ) ) } aggr = state.get_prop_network_aggr((0, 31)) @@ -629,21 +645,27 @@ def test_get_prop_network_aggr_computes_correctly(): def test_get_att_network_aggr_raises_error_for_invalid_accumulator(): state = State() - state.att_data = {(0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 15)})} + state.data = { + (0, 31): Duties(attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 15)})) + } with pytest.raises(ValueError, match="Invalid accumulator"): state.get_att_network_aggr((0, 31)) def test_get_prop_network_aggr_raises_error_for_invalid_accumulator(): state = State() - state.prop_data = {(0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 15)})} + state.data = { + (0, 31): Duties(proposals=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 15)})) + } with pytest.raises(ValueError, match="Invalid accumulator"): state.get_prop_network_aggr((0, 31)) def test_get_sync_network_aggr_raises_error_for_invalid_accumulator(): state = State() - state.sync_data = {(0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 15)})} + state.data = { + (0, 31): Duties(syncs=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 15)})) + } with pytest.raises(ValueError, match="Invalid accumulator"): state.get_sync_network_aggr((0, 31)) @@ -668,7 +690,7 @@ def test_get_sync_network_aggr_raises_error_for_missing_frame_data(): def test_get_att_network_aggr_handles_empty_frame_data(): state = State() - state.att_data = {(0, 31): defaultdict(DutyAccumulator)} + state.data = {(0, 31): Duties()} aggr = state.get_att_network_aggr((0, 31)) assert aggr.assigned == 0 assert aggr.included == 0 @@ -676,7 +698,7 @@ def test_get_att_network_aggr_handles_empty_frame_data(): def test_get_prop_network_aggr_handles_empty_frame_data(): state = State() - state.prop_data = {(0, 31): defaultdict(DutyAccumulator)} + state.data = {(0, 31): Duties()} aggr = state.get_prop_network_aggr((0, 31)) assert aggr.assigned == 0 assert aggr.included == 0 @@ -684,7 +706,7 @@ def test_get_prop_network_aggr_handles_empty_frame_data(): def test_get_sync_network_aggr_handles_empty_frame_data(): state = State() - state.sync_data = {(0, 31): defaultdict(DutyAccumulator)} + state.data = {(0, 31): Duties()} aggr = state.get_sync_network_aggr((0, 31)) assert aggr.assigned == 0 assert aggr.included == 0 From 5092e14d2def7a33768aedcfcd577b46f5dd247f Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Wed, 26 Feb 2025 19:09:16 +0100 Subject: [PATCH 074/162] fix: test_checkpoint --- tests/modules/csm/test_checkpoint.py | 362 ++++++++++++++++++--------- 1 file changed, 237 insertions(+), 125 deletions(-) diff --git a/tests/modules/csm/test_checkpoint.py b/tests/modules/csm/test_checkpoint.py index bbbd1a9c8..6f3a24902 100644 --- a/tests/modules/csm/test_checkpoint.py +++ b/tests/modules/csm/test_checkpoint.py @@ -1,11 +1,11 @@ from copy import deepcopy from typing import cast -from unittest.mock import Mock +from unittest.mock import Mock, patch import pytest -from faker import Faker import src.modules.csm.checkpoint as checkpoint_module +from src.constants import EPOCHS_PER_SYNC_COMMITTEE_PERIOD from src.modules.csm.checkpoint import ( FrameCheckpoint, FrameCheckpointProcessor, @@ -16,8 +16,14 @@ from src.modules.csm.state import State from src.modules.submodules.types import ChainConfig, FrameConfig from src.providers.consensus.client import ConsensusClient -from src.providers.consensus.types import BeaconSpecResponse, BlockAttestation, SlotAttestationCommittee -from src.types import EpochNumber, SlotNumber, ValidatorIndex +from src.providers.consensus.types import ( + BeaconSpecResponse, + BlockAttestation, + SlotAttestationCommittee, + SyncCommittee, + SyncAggregate, +) +from src.types import EpochNumber, SlotNumber, ValidatorIndex, BlockRoot from src.utils.web3converter import Web3Converter from tests.factory.bitarrays import BitListFactory from tests.factory.configs import ( @@ -27,6 +33,14 @@ FrameConfigFactory, SlotAttestationCommitteeFactory, ) +from src.modules.csm.checkpoint import ( + FrameCheckpointProcessor, + ValidatorDuty, + SlotNumber, + SlotOutOfRootsRange, + SYNC_COMMITTEES_CACHE, + SyncCommitteesCache, +) @pytest.fixture(autouse=True) @@ -116,7 +130,7 @@ def test_checkpoints_iterator_given_checkpoints(converter, l_epoch, r_epoch, fin @pytest.fixture def consensus_client(): - return ConsensusClient('http://localhost', 5 * 60, 5, 5) + return ConsensusClient('http://localhost/', 5 * 60, 5, 5) @pytest.fixture @@ -183,9 +197,12 @@ def test_checkpoints_processor_select_block_roots( finalized_blockstamp, ) roots = processor._get_block_roots(0) - selected = processor._select_block_roots(10, roots, 8192) - assert len(selected) == 64 - assert selected == [f'0x{r}' for r in range(320, 384)] + selected = processor._select_block_roots(roots, 10, 8192) + duty_epoch_roots, next_epoch_roots = selected + assert len(duty_epoch_roots) == 32 + assert len(next_epoch_roots) == 32 + assert duty_epoch_roots == [(r, f'0x{r}') for r in range(320, 352)] + assert next_epoch_roots == [(r, f'0x{r}') for r in range(352, 384)] def test_checkpoints_processor_select_block_roots_out_of_range( @@ -200,8 +217,8 @@ def test_checkpoints_processor_select_block_roots_out_of_range( finalized_blockstamp, ) roots = processor._get_block_roots(0) - with pytest.raises(ValueError, match="Slot is out of the state block roots range"): - processor._select_block_roots(255, roots, 8192) + with pytest.raises(checkpoint_module.SlotOutOfRootsRange, match="Slot is out of the state block roots range"): + processor._select_block_roots(roots, 255, 8192) @pytest.fixture() @@ -298,124 +315,219 @@ def test_checkpoints_processor_process_attestations_undefined_committee( assert v.included is False -@pytest.fixture() -def mock_get_block_attestations(consensus_client, faker: Faker): - def _get_block_attestations(root): - slot = faker.random_int() - attestations = [] - for i in range(0, 64): - attestation = deepcopy(cast(BlockAttestation, BlockAttestationFactory.build())) - attestation.data.slot = SlotNumber(slot) - attestation.data.index = i - attestation.aggregation_bits = '0x' + 'f' * 32 - attestations.append(attestation) - return attestations - - consensus_client.get_block_attestations = Mock(side_effect=_get_block_attestations) - - -@pytest.mark.usefixtures( - "mock_get_state_block_roots", - "mock_get_attestation_committees", - "mock_get_block_attestations", - "mock_get_config_spec", -) -def test_checkpoints_processor_no_eip7549_support( - consensus_client, - converter, - monkeypatch: pytest.MonkeyPatch, -): - state = State() - state.migrate(EpochNumber(0), EpochNumber(255), 256, 1) - processor = FrameCheckpointProcessor( - consensus_client, - state, - converter, - Mock(), - eip7549_supported=False, +@pytest.fixture +def frame_checkpoint_processor(): + cc = Mock() + state = Mock() + converter = Mock() + finalized_blockstamp = Mock(slot_number=SlotNumber(0)) + return FrameCheckpointProcessor(cc, state, converter, finalized_blockstamp) + + +def test_check_duties_processes_epoch_with_attestations_and_sync_committee(frame_checkpoint_processor): + checkpoint_block_roots = [Mock(spec=BlockRoot), None, Mock(spec=BlockRoot)] + checkpoint_slot = SlotNumber(100) + duty_epoch = EpochNumber(10) + duty_epoch_roots = [(SlotNumber(100), Mock(spec=BlockRoot)), (SlotNumber(101), Mock(spec=BlockRoot))] + next_epoch_roots = [(SlotNumber(102), Mock(spec=BlockRoot)), (SlotNumber(103), Mock(spec=BlockRoot))] + frame_checkpoint_processor._prepare_att_committees = Mock(return_value={SlotNumber(100): [ValidatorDuty(1, False)]}) + frame_checkpoint_processor._prepare_propose_duties = Mock( + return_value={SlotNumber(100): ValidatorDuty(1, False), SlotNumber(101): ValidatorDuty(1, False)} + ) + frame_checkpoint_processor._prepare_sync_committee = Mock( + return_value={ + 100: [ValidatorDuty(1, False) for _ in range(32)], + 101: [ValidatorDuty(1, False) for _ in range(32)], + } ) - roots = processor._get_block_roots(SlotNumber(0)) - with monkeypatch.context(): - monkeypatch.setattr( - checkpoint_module, - "is_eip7549_attestation", - Mock(return_value=True), - ) - with pytest.raises(ValueError, match="support is not enabled"): - processor._check_duty(0, roots[:64]) + attestation = Mock() + attestation.data.slot = SlotNumber(100) + attestation.data.index = 0 + attestation.aggregation_bits = "0xff" + attestation.committee_bits = "0xff" -def test_checkpoints_processor_check_duty( - mock_get_state_block_roots, - mock_get_attestation_committees, - mock_get_block_attestations, - mock_get_config_spec, - consensus_client, - converter, -): - state = State() - state.migrate(0, 255, 256, 1) - finalized_blockstamp = ... - processor = FrameCheckpointProcessor( - consensus_client, - state, - converter, - finalized_blockstamp, + sync_aggregate = Mock() + sync_aggregate.sync_committee_bits = "0xff" + + frame_checkpoint_processor.cc.get_block_attestations_and_sync = Mock(return_value=([attestation], sync_aggregate)) + frame_checkpoint_processor.state.unprocessed_epochs = [duty_epoch] + + frame_checkpoint_processor._check_duties( + checkpoint_block_roots, checkpoint_slot, duty_epoch, duty_epoch_roots, next_epoch_roots ) - roots = processor._get_block_roots(0) - processor._check_duties(0, roots[:64]) - assert len(state._processed_epochs) == 1 - assert len(state._epochs_to_process) == 256 - assert len(state.unprocessed_epochs) == 255 - assert len(state.att_data[(0, 255)]) == 2048 * 32 - - -def test_checkpoints_processor_process( - mock_get_state_block_roots, - mock_get_attestation_committees, - mock_get_block_attestations, - mock_get_config_spec, - consensus_client, - converter, -): - state = State() - state.migrate(0, 255, 256, 1) - finalized_blockstamp = ... - processor = FrameCheckpointProcessor( - consensus_client, - state, - converter, - finalized_blockstamp, + + frame_checkpoint_processor.state.increment_att_duty.assert_called() + frame_checkpoint_processor.state.increment_sync_duty.assert_called() + frame_checkpoint_processor.state.increment_prop_duty.assert_called() + + +def test_check_duties_processes_epoch_with_no_attestations(frame_checkpoint_processor): + checkpoint_block_roots = [Mock(spec=BlockRoot), None, Mock(spec=BlockRoot)] + checkpoint_slot = SlotNumber(100) + duty_epoch = EpochNumber(10) + duty_epoch_roots = [(SlotNumber(100), Mock(spec=BlockRoot)), (SlotNumber(101), Mock(spec=BlockRoot))] + next_epoch_roots = [(SlotNumber(102), Mock(spec=BlockRoot)), (SlotNumber(103), Mock(spec=BlockRoot))] + frame_checkpoint_processor._prepare_att_committees = Mock(return_value={}) + frame_checkpoint_processor._prepare_propose_duties = Mock( + return_value={SlotNumber(100): ValidatorDuty(1, False), SlotNumber(101): ValidatorDuty(1, False)} ) - roots = processor._get_block_roots(0) - processor._process([0, 1], {0: roots[:64], 1: roots[32:96]}) - assert len(state._processed_epochs) == 2 - assert len(state._epochs_to_process) == 256 - assert len(state.unprocessed_epochs) == 254 - assert len(state.att_data[(0, 255)]) == 2048 * 32 - - -def test_checkpoints_processor_exec( - mock_get_state_block_roots, - mock_get_attestation_committees, - mock_get_block_attestations, - mock_get_config_spec, - consensus_client, - converter, -): - state = State() - state.migrate(0, 255, 256, 1) - finalized_blockstamp = ... - processor = FrameCheckpointProcessor( - consensus_client, - state, - converter, - finalized_blockstamp, + frame_checkpoint_processor._prepare_sync_committee = Mock( + return_value={100: [ValidatorDuty(1, False)], 101: [ValidatorDuty(1, False)]} + ) + + sync_aggregate = Mock() + sync_aggregate.sync_committee_bits = "0x00" + + frame_checkpoint_processor.cc.get_block_attestations_and_sync = Mock(return_value=([], sync_aggregate)) + frame_checkpoint_processor.state.unprocessed_epochs = [duty_epoch] + + frame_checkpoint_processor._check_duties( + checkpoint_block_roots, checkpoint_slot, duty_epoch, duty_epoch_roots, next_epoch_roots + ) + + assert frame_checkpoint_processor.state.increment_att_duty.call_count == 0 + assert frame_checkpoint_processor.state.increment_sync_duty.call_count == 2 + assert frame_checkpoint_processor.state.increment_prop_duty.call_count == 2 + + +def test_prepare_sync_committee_returns_duties_for_valid_sync_committee(frame_checkpoint_processor): + epoch = EpochNumber(10) + duty_block_roots = [(SlotNumber(100), Mock()), (SlotNumber(101), Mock())] + sync_committee = Mock(spec=SyncCommittee) + sync_committee.validators = [1, 2, 3] + frame_checkpoint_processor._get_sync_committee = Mock(return_value=sync_committee) + + duties = frame_checkpoint_processor._prepare_sync_committee(epoch, duty_block_roots) + + expected_duties = { + SlotNumber(100): [ + ValidatorDuty(validator_index=1, included=False), + ValidatorDuty(validator_index=2, included=False), + ValidatorDuty(validator_index=3, included=False), + ], + SlotNumber(101): [ + ValidatorDuty(validator_index=1, included=False), + ValidatorDuty(validator_index=2, included=False), + ValidatorDuty(validator_index=3, included=False), + ], + } + assert duties == expected_duties + + +def test_prepare_sync_committee_skips_duties_for_missed_slots(frame_checkpoint_processor): + epoch = EpochNumber(10) + duty_block_roots = [(SlotNumber(100), None), (SlotNumber(101), Mock())] + sync_committee = Mock(spec=SyncCommittee) + sync_committee.validators = [1, 2, 3] + frame_checkpoint_processor._get_sync_committee = Mock(return_value=sync_committee) + + duties = frame_checkpoint_processor._prepare_sync_committee(epoch, duty_block_roots) + + expected_duties = { + SlotNumber(101): [ + ValidatorDuty(validator_index=1, included=False), + ValidatorDuty(validator_index=2, included=False), + ValidatorDuty(validator_index=3, included=False), + ] + } + assert duties == expected_duties + + +def test_get_sync_committee_returns_cached_sync_committee(frame_checkpoint_processor): + epoch = EpochNumber(10) + sync_committee_period = epoch // EPOCHS_PER_SYNC_COMMITTEE_PERIOD + cached_sync_committee = Mock(spec=SyncCommittee) + + with patch('src.modules.csm.checkpoint.SYNC_COMMITTEES_CACHE', {sync_committee_period: cached_sync_committee}): + result = frame_checkpoint_processor._get_sync_committee(epoch) + assert result == cached_sync_committee + + +def test_get_sync_committee_fetches_and_caches_when_not_cached(frame_checkpoint_processor): + epoch = EpochNumber(10) + sync_committee_period = epoch // EPOCHS_PER_SYNC_COMMITTEE_PERIOD + sync_committee = Mock(spec=SyncCommittee) + sync_committee.validators = [1, 2, 3] + frame_checkpoint_processor.converter.get_epoch_first_slot = Mock(return_value=SlotNumber(0)) + frame_checkpoint_processor.cc.get_sync_committee = Mock(return_value=sync_committee) + + result = frame_checkpoint_processor._get_sync_committee(epoch) + + assert result.validators == sync_committee.validators + assert SYNC_COMMITTEES_CACHE[sync_committee_period].validators == sync_committee.validators + + +def test_get_sync_committee_handles_cache_eviction(frame_checkpoint_processor): + epoch = EpochNumber(10) + sync_committee_period = epoch // EPOCHS_PER_SYNC_COMMITTEE_PERIOD + old_sync_committee_period = sync_committee_period - 1 + old_sync_committee = Mock(spec=SyncCommittee) + sync_committee = Mock(spec=SyncCommittee) + frame_checkpoint_processor.converter.get_epoch_first_slot = Mock(return_value=SlotNumber(0)) + frame_checkpoint_processor.cc.get_sync_committee = Mock(return_value=sync_committee) + + with patch('src.modules.csm.checkpoint.SYNC_COMMITTEES_CACHE', SyncCommitteesCache()) as cache: + cache.max_size = 1 + cache[old_sync_committee_period] = old_sync_committee + + result = frame_checkpoint_processor._get_sync_committee(epoch) + + assert result == sync_committee + assert sync_committee_period in SYNC_COMMITTEES_CACHE + assert old_sync_committee_period not in SYNC_COMMITTEES_CACHE + + +def test_prepare_propose_duties(frame_checkpoint_processor): + epoch = EpochNumber(10) + checkpoint_block_roots = [Mock(spec=BlockRoot), None, Mock(spec=BlockRoot)] + checkpoint_slot = SlotNumber(100) + dependent_root = Mock(spec=BlockRoot) + frame_checkpoint_processor._get_dependent_root_for_proposer_duties = Mock(return_value=dependent_root) + proposer_duty1 = Mock(slot=SlotNumber(101), validator_index=1) + proposer_duty2 = Mock(slot=SlotNumber(102), validator_index=2) + frame_checkpoint_processor.cc.get_proposer_duties = Mock(return_value=[proposer_duty1, proposer_duty2]) + + duties = frame_checkpoint_processor._prepare_propose_duties(epoch, checkpoint_block_roots, checkpoint_slot) + + expected_duties = { + SlotNumber(101): ValidatorDuty(validator_index=1, included=False), + SlotNumber(102): ValidatorDuty(validator_index=2, included=False), + } + assert duties == expected_duties + + +def test_get_dependent_root_for_proposer_duties_from_state_block_roots(frame_checkpoint_processor): + epoch = EpochNumber(10) + checkpoint_block_roots = [Mock(spec=BlockRoot), None, Mock(spec=BlockRoot)] + checkpoint_slot = SlotNumber(100) + dependent_slot = SlotNumber(99) + frame_checkpoint_processor.converter.get_epoch_last_slot = Mock(return_value=dependent_slot) + frame_checkpoint_processor._select_block_root_by_slot = Mock(return_value=checkpoint_block_roots[2]) + + dependent_root = frame_checkpoint_processor._get_dependent_root_for_proposer_duties( + epoch, checkpoint_block_roots, checkpoint_slot ) - iterator = FrameCheckpointsIterator(converter, 0, 1, 255) - for checkpoint in iterator: - processor.exec(checkpoint) - assert len(state._processed_epochs) == 2 - assert len(state._epochs_to_process) == 256 - assert len(state.unprocessed_epochs) == 254 - assert len(state.att_data[(0, 255)]) == 2048 * 32 + + assert dependent_root == checkpoint_block_roots[2] + + +def test_get_dependent_root_for_proposer_duties_from_cl_when_slot_out_of_range(frame_checkpoint_processor): + epoch = EpochNumber(10) + checkpoint_block_roots = [Mock(spec=BlockRoot), None, Mock(spec=BlockRoot)] + checkpoint_slot = SlotNumber(100) + dependent_slot = SlotNumber(99) + frame_checkpoint_processor.converter.get_epoch_last_slot = Mock(return_value=dependent_slot) + frame_checkpoint_processor._select_block_root_by_slot = Mock(side_effect=SlotOutOfRootsRange) + non_missed_slot = SlotNumber(98) + + prev_slot_response = Mock() + prev_slot_response.message.slot = non_missed_slot + with patch('src.modules.csm.checkpoint.get_prev_non_missed_slot', Mock(return_value=prev_slot_response)): + frame_checkpoint_processor.cc.get_block_root = Mock(return_value=Mock(root=checkpoint_block_roots[0])) + + dependent_root = frame_checkpoint_processor._get_dependent_root_for_proposer_duties( + epoch, checkpoint_block_roots, checkpoint_slot + ) + + assert dependent_root == checkpoint_block_roots[0] From 8429ef927597081bc654fddbe3f5f082f5384d38 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Wed, 26 Feb 2025 19:11:43 +0100 Subject: [PATCH 075/162] feat: remove `eip7549_supported` --- src/modules/csm/checkpoint.py | 10 ++-------- src/modules/csm/csm.py | 3 +-- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/modules/csm/checkpoint.py b/src/modules/csm/checkpoint.py index c3be7e275..bd5d60a0f 100644 --- a/src/modules/csm/checkpoint.py +++ b/src/modules/csm/checkpoint.py @@ -133,21 +133,17 @@ class FrameCheckpointProcessor: state: State finalized_blockstamp: BlockStamp - eip7549_supported: bool - def __init__( self, cc: ConsensusClient, state: State, converter: Web3Converter, finalized_blockstamp: BlockStamp, - eip7549_supported: bool = True, ): self.cc = cc self.converter = converter self.state = state self.finalized_blockstamp = finalized_blockstamp - self.eip7549_supported = eip7549_supported def exec(self, checkpoint: FrameCheckpoint) -> int: logger.info( @@ -246,12 +242,13 @@ def _check_duties( att_committees = self._prepare_att_committees(duty_epoch) propose_duties = self._prepare_propose_duties(duty_epoch, checkpoint_block_roots, checkpoint_slot) sync_committees = self._prepare_sync_committee(duty_epoch, duty_epoch_roots) + for slot, root in [*duty_epoch_roots, *next_epoch_roots]: missed_slot = root is None if missed_slot: continue attestations, sync_aggregate = self.cc.get_block_attestations_and_sync(root) - process_attestations(attestations, att_committees, self.eip7549_supported) + process_attestations(attestations, att_committees) if (slot, root) in duty_epoch_roots: propose_duties[slot].included = True process_sync(slot, sync_aggregate, sync_committees) @@ -409,11 +406,8 @@ def process_sync(slot: SlotNumber, sync_aggregate: SyncAggregate, committees: Sy def process_attestations( attestations: Iterable[BlockAttestation], committees: AttestationCommittees, - eip7549_supported: bool = True, ) -> None: for attestation in attestations: - if is_eip7549_attestation(attestation) and not eip7549_supported: - raise ValueError("EIP-7549 support is not enabled") committee_offset = 0 for committee_idx in get_committee_indices(attestation): committee = committees.get((attestation.data.slot, committee_idx), []) diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index 0081fc59c..99fa24e84 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -161,7 +161,6 @@ def collect_data(self, blockstamp: BlockStamp) -> bool: """Ongoing report data collection for the estimated reference slot""" consensus_version = self.get_consensus_version(blockstamp) - eip7549_supported = consensus_version != 1 logger.info({"msg": "Collecting data for the report"}) @@ -207,7 +206,7 @@ def collect_data(self, blockstamp: BlockStamp) -> bool: except MinStepIsNotReached: return False - processor = FrameCheckpointProcessor(self.w3.cc, self.state, converter, blockstamp, eip7549_supported) + processor = FrameCheckpointProcessor(self.w3.cc, self.state, converter, blockstamp) for checkpoint in checkpoints: if self.get_epochs_range_to_process(self._receive_last_finalized_slot()) != (l_epoch, r_epoch): From 059c621aeff88f81d16a4596083e481263863052 Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Wed, 26 Feb 2025 21:51:15 +0100 Subject: [PATCH 076/162] feat: customized decoder for RewardsTree --- src/modules/csm/tree.py | 27 +++++++++++++++++++++++++++ tests/modules/csm/test_tree.py | 5 +---- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/modules/csm/tree.py b/src/modules/csm/tree.py index 4b3179391..42083bbdb 100644 --- a/src/modules/csm/tree.py +++ b/src/modules/csm/tree.py @@ -70,7 +70,32 @@ def new(cls, values: Sequence[LeafType]) -> Self: raise NotImplementedError +class RewardsTreeJSONDecoder(TreeJSONDecoder): + # NOTE: object_pairs_hook is set unconditionally upon object initialisation, so it's required to + # override the __init__ method. + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs, object_pairs_hook=self.__object_pairs_hook) + + @staticmethod + def __object_pairs_hook(items: list[tuple[str, Any]]): + def try_decode_value(key: str, obj: Any): + if key != "value": + return obj + if not isinstance(obj, list) or not len(obj) == 2: + raise ValueError(f"Unexpected RewardsTreeLeaf value given {obj=}") + no_id, shares = obj + if not isinstance(no_id, int): + raise ValueError(f"Unexpected RewardsTreeLeaf value given {obj=}") + if not isinstance(shares, int): + raise ValueError(f"Unexpected RewardsTreeLeaf value given {obj=}") + return no_id, shares + + return {k: try_decode_value(k, v) for k, v in items} + + class RewardsTree(Tree[RewardsTreeLeaf]): + decoder = RewardsTreeJSONDecoder + @classmethod def new(cls, values) -> Self: """Create new instance around the wrapped tree out of the given values""" @@ -98,6 +123,8 @@ def try_decode_value(key: str, obj: Any): if not isinstance(obj, list) or not len(obj) == 3: raise ValueError(f"Unexpected StrikesTreeLeaf value given {obj=}") no_id, pubkey, strikes = obj + if not isinstance(no_id, int): + raise ValueError(f"Unexpected StrikesTreeLeaf value given {obj=}") if not isinstance(pubkey, str) or not pubkey.startswith("0x"): raise ValueError(f"Unexpected StrikesTreeLeaf value given {obj=}") if not isinstance(strikes, list): diff --git a/tests/modules/csm/test_tree.py b/tests/modules/csm/test_tree.py index 6dd23a487..fee2dd197 100644 --- a/tests/modules/csm/test_tree.py +++ b/tests/modules/csm/test_tree.py @@ -39,12 +39,9 @@ def test_non_null_root(self, tree: TreeType): def test_encode_decode(self, tree: TreeType): decoded = self.cls.decode(tree.encode()) + assert decoded.values == self.values assert decoded.root == tree.root - json_encode = self.encoder.encode - decoded_values = [v["value"] for v in decoded.tree.values] - assert json_encode(decoded_values) == json_encode(self.values) - def test_decode_plain_tree_dump(self, tree: TreeType): decoded = self.cls.decode(self.encoder.encode(tree.tree.dump()).encode()) assert decoded.root == tree.root From 92f9c62d7ce0afb111e9a13fdac6479eb8039a1b Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Wed, 26 Feb 2025 21:52:37 +0100 Subject: [PATCH 077/162] refactor: factor out LastReport --- src/modules/csm/csm.py | 210 +++++++++++---------- src/providers/consensus/types.py | 2 +- tests/modules/csm/test_csm_distribution.py | 76 +++++--- tests/modules/csm/test_csm_module.py | 149 +++++++++------ 4 files changed, 252 insertions(+), 185 deletions(-) diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index 8cd3487eb..2ece93fbe 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -1,9 +1,10 @@ import logging from collections import defaultdict -from typing import Iterable +from dataclasses import dataclass +from functools import cached_property +from typing import Iterable, Self from hexbytes import HexBytes -from copy import deepcopy from src.constants import TOTAL_BASIS_POINTS, UINT64_MAX from src.metrics.prometheus.business import CONTRACT_ON_PAUSE @@ -32,7 +33,6 @@ ) from src.utils.cache import global_lru_cache as lru_cache from src.utils.slot import get_reference_blockstamp -from src.utils.types import hex_str_to_bytes from src.utils.web3converter import Web3Converter from src.web3py.extensions.lido_validators import LidoValidator, NodeOperatorId, StakingModule, ValidatorsByNodeOperator from src.web3py.types import Web3 @@ -98,55 +98,35 @@ def execute_module(self, last_finalized_blockstamp: BlockStamp) -> ModuleExecute def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: self.validate_state(blockstamp) - prev_rewards_root = self.w3.csm.get_rewards_tree_root(blockstamp) - prev_rewards_cid = self.w3.csm.get_rewards_tree_cid(blockstamp) + last_report = self._get_last_report(blockstamp) + rewards_tree_root, rewards_cid = last_report.rewards_tree_root, last_report.rewards_tree_cid + strikes_tree_root, strikes_cid = last_report.strikes_tree_root, last_report.strikes_tree_cid - if (prev_rewards_cid is None) != (prev_rewards_root == ZERO_HASH): - raise InconsistentData(f"Got inconsistent previous tree data: {prev_rewards_root=} {prev_rewards_cid=}") - - prev_strikes_root = self.w3.csm.get_strikes_tree_root(blockstamp) - prev_strikes_cid = self.w3.csm.get_strikes_tree_cid(blockstamp) - - if (prev_strikes_cid is None) != (prev_strikes_root == ZERO_HASH): - raise InconsistentData(f"Got inconsistent previous tree data: {prev_strikes_root=} {prev_strikes_cid=}") - - total_distributed, total_rewards, strikes_per_frame, logs = self.calculate_distribution(blockstamp) - log_cid = self.publish_log(logs) - - rewards_tree_root, rewards_cid = prev_rewards_root, prev_rewards_cid - strikes_tree_root, strikes_cid = prev_strikes_root, prev_strikes_cid + ( + total_distributed, + total_rewards, + strikes, + logs, + ) = self.calculate_distribution(blockstamp, last_report) if total_distributed: - if prev_rewards_cid and prev_rewards_root != ZERO_HASH: - # Update cumulative amount of stETH shares for all operators. - for no_id, accumulated_rewards in self.get_accumulated_rewards(prev_rewards_cid, prev_rewards_root): - total_rewards[no_id] += accumulated_rewards - else: - logger.info({"msg": "No previous distribution. Nothing to accumulate"}) - rewards_tree = self.make_rewards_tree(total_rewards) rewards_tree_root = rewards_tree.root rewards_cid = self.publish_tree(rewards_tree) - historical_strikes = {} - if prev_strikes_cid and prev_strikes_root != ZERO_HASH: - historical_strikes = self.get_accumulated_strikes(prev_strikes_cid, prev_strikes_root) + if strikes: + strikes_tree = self.make_strikes_tree(strikes) + strikes_tree_root = strikes_tree.root + strikes_cid = self.publish_tree(strikes_tree) - strikes = self._merge_strikes( - historical_strikes, - strikes_per_frame, - blockstamp, - ) - strikes_tree = self.make_strikes_tree(strikes) - strikes_tree_root = strikes_tree.root - strikes_cid = self.publish_tree(strikes_tree) + logs_cid = self.publish_log(logs) return ReportData( self.get_consensus_version(blockstamp), blockstamp.ref_slot, tree_root=rewards_tree_root, tree_cid=rewards_cid or "", - log_cid=log_cid, + log_cid=logs_cid, distributed=total_distributed, strikes_tree_root=strikes_tree_root, strikes_tree_cid=strikes_cid or "", @@ -240,20 +220,19 @@ def collect_data(self, blockstamp: BlockStamp) -> bool: return self.state.is_fulfilled def calculate_distribution( - self, - blockstamp: ReferenceBlockStamp, + self, blockstamp: ReferenceBlockStamp, last_report: "LastReport" ) -> tuple[ Shares, defaultdict[NodeOperatorId, Shares], - dict[Frame, dict[StrikesValidator, int]], + dict[StrikesValidator, StrikesList], list[FramePerfLog], ]: """Computes distribution of fee shares at the given timestamp""" operators_to_validators = self.module_validators_by_node_operators(blockstamp) - total_distributed = Shares(0) total_rewards = defaultdict[NodeOperatorId, Shares](Shares) - strikes_per_frame: dict[Frame, dict[StrikesValidator, int]] = {} + total_distributed = Shares(0) + strikes = last_report.strikes logs: list[FramePerfLog] = [] for frame in self.state.frames: @@ -281,7 +260,7 @@ def calculate_distribution( if not distributed_in_frame: logger.info({"msg": f"No rewards distributed in frame [{from_epoch};{to_epoch}]"}) - strikes_per_frame[frame] = strikes_in_frame + self._merge_strikes(strikes, strikes_in_frame, frame_blockstamp) if not strikes_in_frame: logger.info({"msg": f"No strikes in frame [{from_epoch};{to_epoch}]"}) @@ -297,7 +276,13 @@ def calculate_distribution( if total_distributed != sum(total_rewards.values()): raise InconsistentData(f"Invalid distribution: {sum(total_rewards.values())=} != {total_distributed=}") - return total_distributed, total_rewards, strikes_per_frame, logs + for no_id, rewards in last_report.rewards: + total_rewards[no_id] += rewards + + return total_distributed, total_rewards, strikes, logs + + def _get_last_report(self, blockstamp: BlockStamp) -> "LastReport": + return LastReport.load(self.w3, blockstamp) def _get_ref_blockstamp_for_frame( self, blockstamp: ReferenceBlockStamp, frame_ref_epoch: EpochNumber @@ -400,59 +385,26 @@ def calc_rewards_distribution_in_frame( return rewards_distribution - def get_accumulated_rewards(self, cid: CID, root: HexBytes) -> Iterable[RewardsTreeLeaf]: - logger.info({"msg": "Fetching tree by CID from IPFS", "cid": repr(cid)}) - tree = RewardsTree.decode(self.w3.ipfs.fetch(cid)) - - logger.info({"msg": "Restored tree from IPFS dump", "root": repr(tree.root)}) - - if tree.root != root: - raise ValueError("Unexpected tree root got from IPFS dump") - - return tree.values - - def get_accumulated_strikes(self, cid: CID, root: HexBytes) -> dict[StrikesValidator, StrikesList]: - logger.info({"msg": "Fetching tree by CID from IPFS", "cid": repr(cid)}) - tree = StrikesTree.decode(self.w3.ipfs.fetch(cid)) - - logger.info({"msg": "Restored tree from IPFS dump", "root": repr(tree.root)}) - - if tree.root != root: - raise ValueError("Unexpected tree root got from IPFS dump") - - return {(no_id, pubkey): strikes for no_id, pubkey, strikes in tree.values} - def _merge_strikes( self, - historical_strikes: dict[StrikesValidator, StrikesList], - strikes_per_frame: dict[Frame, dict[StrikesValidator, int]], - blockstamp: ReferenceBlockStamp, - ) -> dict[StrikesValidator, StrikesList]: - out = deepcopy(historical_strikes) - - for frame in self.state.frames: - strikes_in_frame = strikes_per_frame[frame] - for key in strikes_in_frame: - if key not in out: - out[key] = StrikesList() - out[key].push(strikes_in_frame[key]) - - _, to_epoch = frame - frame_blockstamp = blockstamp - if to_epoch != blockstamp.ref_epoch: - frame_blockstamp = self._get_ref_blockstamp_for_frame(blockstamp, to_epoch) - - for key in out: - no_id, _ = key - if key not in strikes_in_frame: - out[key].push(StrikesList.SENTINEL) # Just shifting... - maxlen = self.w3.csm.get_strikes_params(no_id, frame_blockstamp).lifetime - out[key].resize(maxlen) - # NOTE: Cleanup sequences like [0,0,0] since they don't bring any information. - if not sum(out[key]): - del out[key] - - return out + acc: dict[StrikesValidator, StrikesList], + strikes_in_frame: dict[StrikesValidator, int], + frame_blockstamp: ReferenceBlockStamp, + ) -> None: + for key in strikes_in_frame: + if key not in acc: + acc[key] = StrikesList() + acc[key].push(strikes_in_frame[key]) + + for key in acc: + no_id, _ = key + if key not in strikes_in_frame: + acc[key].push(StrikesList.SENTINEL) # Just shifting... + maxlen = self.w3.csm.get_strikes_params(no_id, frame_blockstamp).lifetime + acc[key].resize(maxlen) + # NOTE: Cleanup sequences like [0,0,0] since they don't bring any information. + if not sum(acc[key]): + del acc[key] def make_rewards_tree(self, shares: dict[NodeOperatorId, Shares]) -> RewardsTree: if not shares: @@ -556,3 +508,69 @@ def _get_staking_module(self) -> StakingModule: return mod raise NoModuleFound + + +@dataclass +class LastReport: + w3: Web3 + blockstamp: BlockStamp + + rewards_tree_root: HexBytes + strikes_tree_root: HexBytes + rewards_tree_cid: CID | None + strikes_tree_cid: CID | None + + @classmethod + def load(cls, w3: Web3, blockstamp: BlockStamp) -> Self: + rewards_tree_root = w3.csm.get_rewards_tree_root(blockstamp) + rewards_tree_cid = w3.csm.get_rewards_tree_cid(blockstamp) + + if (rewards_tree_cid is None) != (rewards_tree_root == ZERO_HASH): + raise InconsistentData(f"Got inconsistent previous tree data: {rewards_tree_root=} {rewards_tree_cid=}") + + strikes_tree_root = w3.csm.get_strikes_tree_root(blockstamp) + strikes_tree_cid = w3.csm.get_strikes_tree_cid(blockstamp) + + if (strikes_tree_cid is None) != (strikes_tree_root == ZERO_HASH): + raise InconsistentData(f"Got inconsistent previous tree data: {strikes_tree_root=} {strikes_tree_cid=}") + + return cls( + w3, + blockstamp, + rewards_tree_root, + strikes_tree_root, + rewards_tree_cid, + strikes_tree_cid, + ) + + @cached_property + def rewards(self) -> Iterable[RewardsTreeLeaf]: + if self.rewards_tree_cid is None or self.rewards_tree_root == ZERO_HASH: + logger.info({"msg": f"No rewards distribution as of {self.blockstamp=}."}) + return [] + + logger.info({"msg": "Fetching rewards tree by CID from IPFS", "cid": repr(self.rewards_tree_cid)}) + tree = RewardsTree.decode(self.w3.ipfs.fetch(self.rewards_tree_cid)) + + logger.info({"msg": "Restored rewards tree from IPFS dump", "root": repr(tree.root)}) + + if tree.root != self.rewards_tree_root: + raise ValueError("Unexpected rewards tree root got from IPFS dump") + + return tree.values + + @cached_property + def strikes(self) -> dict[StrikesValidator, StrikesList]: + if self.strikes_tree_cid is None or self.strikes_tree_root == ZERO_HASH: + logger.info({"msg": f"No strikes reported as of {self.blockstamp=}."}) + return {} + + logger.info({"msg": "Fetching tree by CID from IPFS", "cid": repr(self.strikes_tree_cid)}) + tree = StrikesTree.decode(self.w3.ipfs.fetch(self.strikes_tree_cid)) + + logger.info({"msg": "Restored strikes tree from IPFS dump", "root": repr(tree.root)}) + + if tree.root != self.strikes_tree_root: + raise ValueError("Unexpected strikes tree root got from IPFS dump") + + return {(no_id, pubkey): strikes for no_id, pubkey, strikes in tree.values} diff --git a/src/providers/consensus/types.py b/src/providers/consensus/types.py index ee9c0153c..f6203a6f7 100644 --- a/src/providers/consensus/types.py +++ b/src/providers/consensus/types.py @@ -1,8 +1,8 @@ from dataclasses import dataclass, field from typing import Protocol -from ens.ens import HexBytes from eth_typing import BlockNumber +from hexbytes import HexBytes from web3.types import Timestamp from src.constants import FAR_FUTURE_EPOCH diff --git a/tests/modules/csm/test_csm_distribution.py b/tests/modules/csm/test_csm_distribution.py index 1fdbf0e6b..15000eae5 100644 --- a/tests/modules/csm/test_csm_distribution.py +++ b/tests/modules/csm/test_csm_distribution.py @@ -5,8 +5,8 @@ from web3.types import Wei from src.constants import UINT64_MAX -from src.modules.csm.csm import CSMError, CSOracle -from src.modules.csm.log import ValidatorFrameSummary +from src.modules.csm.csm import CSMError, CSOracle, LastReport +from src.modules.csm.log import FramePerfLog, ValidatorFrameSummary from src.modules.csm.state import AttestationsAccumulator, State from src.types import NodeOperatorId, ValidatorIndex from src.web3py.extensions import CSM @@ -24,83 +24,102 @@ def module(web3, csm: CSM): yield CSOracle(web3) -def test_calculate_distribution_handles_single_frame(module): +def test_calculate_distribution_handles_single_frame(module: CSOracle): module.state = Mock() module.state.frames = [(1, 2)] blockstamp = Mock() + last_report = Mock(strikes={}, rewards=[]) module.module_validators_by_node_operators = Mock() module._get_ref_blockstamp_for_frame = Mock(return_value=blockstamp) + module._get_performance_threshold = Mock(return_value=1) module.w3.csm.fee_distributor.shares_to_distribute = Mock(return_value=500) - module._calculate_distribution_in_frame = Mock(return_value=({NodeOperatorId(1): 500}, Mock())) + module._calculate_distribution_in_frame = Mock(return_value=({NodeOperatorId(1): 500}, {})) - total_distributed, total_rewards, logs = module.calculate_distribution(blockstamp) + ( + total_distributed, + total_rewards, + strikes, + logs, + ) = module.calculate_distribution(blockstamp, last_report) assert total_distributed == 500 assert total_rewards[NodeOperatorId(1)] == 500 assert len(logs) == 1 + assert not strikes -def test_calculate_distribution_handles_multiple_frames(module): +def test_calculate_distribution_handles_multiple_frames(module: CSOracle): module.state = Mock() module.state.frames = [(1, 2), (3, 4), (5, 6)] blockstamp = ReferenceBlockStampFactory.build(ref_epoch=2) + last_report = Mock(strikes={}, rewards=[]) module.module_validators_by_node_operators = Mock() module._get_ref_blockstamp_for_frame = Mock(return_value=blockstamp) + module._get_performance_threshold = Mock(return_value=1) module.w3.csm.fee_distributor.shares_to_distribute = Mock(return_value=800) module._calculate_distribution_in_frame = Mock( side_effect=[ - ({NodeOperatorId(1): 500}, Mock()), - ({NodeOperatorId(1): 136}, Mock()), - ({NodeOperatorId(1): 164}, Mock()), + ({NodeOperatorId(1): 500}, {}), + ({NodeOperatorId(1): 136}, {}), + ({NodeOperatorId(1): 164}, {}), ] ) - total_distributed, total_rewards, logs = module.calculate_distribution(blockstamp) + ( + total_distributed, + total_rewards, + strikes, + logs, + ) = module.calculate_distribution(blockstamp, last_report) assert total_distributed == 800 assert total_rewards[NodeOperatorId(1)] == 800 assert len(logs) == 3 + assert not strikes module._get_ref_blockstamp_for_frame.assert_has_calls( [call(blockstamp, frame[1]) for frame in module.state.frames[1:]] ) -def test_calculate_distribution_handles_invalid_distribution(module): +def test_calculate_distribution_handles_invalid_distribution(module: CSOracle): module.state = Mock() module.state.frames = [(1, 2)] blockstamp = Mock() + last_report = Mock(strikes={}, rewards=[]) module.module_validators_by_node_operators = Mock() module._get_ref_blockstamp_for_frame = Mock(return_value=blockstamp) + module._get_performance_threshold = Mock(return_value=1) module.w3.csm.fee_distributor.shares_to_distribute = Mock(return_value=500) - module._calculate_distribution_in_frame = Mock(return_value=({NodeOperatorId(1): 600}, Mock())) + module._calculate_distribution_in_frame = Mock(return_value=({NodeOperatorId(1): 600}, {})) with pytest.raises(CSMError, match="Invalid distribution"): - module.calculate_distribution(blockstamp) + module.calculate_distribution(blockstamp, last_report) -def test_calculate_distribution_in_frame_handles_no_attestation_duty(module): +def test_calculate_distribution_in_frame_handles_no_attestation_duty(module: CSOracle): frame = Mock() - blockstamp = Mock() + threshold = 1.0 rewards_to_distribute = UINT64_MAX validator = LidoValidatorFactory.build() node_operator_id = validator.lido_id.operatorIndex operators_to_validators = {(Mock(), node_operator_id): [validator]} module.state = State() module.state.data = {frame: defaultdict(AttestationsAccumulator)} - module._get_performance_threshold = Mock() + log = FramePerfLog(Mock(), frame) - rewards_distribution, log = module._calculate_distribution_in_frame( - frame, blockstamp, rewards_to_distribute, operators_to_validators + rewards_distribution, strikes_in_frame = module._calculate_distribution_in_frame( + frame, threshold, rewards_to_distribute, operators_to_validators, log ) assert rewards_distribution[node_operator_id] == 0 assert log.operators[node_operator_id].distributed == 0 assert log.operators[node_operator_id].validators == defaultdict(ValidatorFrameSummary) + assert not strikes_in_frame -def test_calculate_distribution_in_frame_handles_above_threshold_performance(module): +def test_calculate_distribution_in_frame_handles_above_threshold_performance(module: CSOracle): frame = Mock() - blockstamp = Mock() + threshold = 0.5 rewards_to_distribute = UINT64_MAX validator = LidoValidatorFactory.build() validator.validator.slashed = False @@ -109,20 +128,21 @@ def test_calculate_distribution_in_frame_handles_above_threshold_performance(mod module.state = State() attestation_duty = AttestationsAccumulator(assigned=10, included=6) module.state.data = {frame: {validator.index: attestation_duty}} - module._get_performance_threshold = Mock(return_value=0.5) + log = FramePerfLog(Mock(), frame) - rewards_distribution, log = module._calculate_distribution_in_frame( - frame, blockstamp, rewards_to_distribute, operators_to_validators + rewards_distribution, strikes_in_frame = module._calculate_distribution_in_frame( + frame, threshold, rewards_to_distribute, operators_to_validators, log ) assert rewards_distribution[node_operator_id] > 0 # no need to check exact value assert log.operators[node_operator_id].distributed > 0 assert log.operators[node_operator_id].validators[validator.index].attestation_duty == attestation_duty + assert not strikes_in_frame -def test_calculate_distribution_in_frame_handles_below_threshold_performance(module): +def test_calculate_distribution_in_frame_handles_below_threshold_performance(module: CSOracle): frame = Mock() - blockstamp = Mock() + threshold = 0.5 rewards_to_distribute = UINT64_MAX validator = LidoValidatorFactory.build() validator.validator.slashed = False @@ -132,14 +152,16 @@ def test_calculate_distribution_in_frame_handles_below_threshold_performance(mod attestation_duty = AttestationsAccumulator(assigned=10, included=5) module.state.data = {frame: {validator.index: attestation_duty}} module._get_performance_threshold = Mock(return_value=0.5) + log = FramePerfLog(Mock(), frame) - rewards_distribution, log = module._calculate_distribution_in_frame( - frame, blockstamp, rewards_to_distribute, operators_to_validators + rewards_distribution, strikes_in_frame = module._calculate_distribution_in_frame( + frame, threshold, rewards_to_distribute, operators_to_validators, log ) assert rewards_distribution[node_operator_id] == 0 assert log.operators[node_operator_id].distributed == 0 assert log.operators[node_operator_id].validators[validator.index].attestation_duty == attestation_duty + assert (node_operator_id, validator.pubkey) in strikes_in_frame def test_performance_threshold_calculates_correctly(module): diff --git a/tests/modules/csm/test_csm_module.py b/tests/modules/csm/test_csm_module.py index c80de865a..c7936a852 100644 --- a/tests/modules/csm/test_csm_module.py +++ b/tests/modules/csm/test_csm_module.py @@ -8,7 +8,7 @@ from hexbytes import HexBytes from src.constants import UINT64_MAX -from src.modules.csm.csm import CSOracle, StrikesValidator +from src.modules.csm.csm import CSOracle, LastReport, StrikesValidator from src.modules.csm.state import State from src.modules.csm.tree import RewardsTree, Tree from src.modules.csm.types import RewardsTreeLeaf, StrikesList @@ -17,6 +17,7 @@ from src.providers.ipfs import CID, CIDv0 from src.types import NodeOperatorId, SlotNumber from src.web3py.extensions.csm import CSM +from src.web3py.types import Web3 from tests.factory.blockstamp import ReferenceBlockStampFactory from tests.factory.configs import ChainConfigFactory, FrameConfigFactory @@ -369,12 +370,7 @@ def test_collect_data_fulfilled_state( @dataclass(frozen=True) class BuildReportTestParam: - prev_rewards_tree_root: HexBytes - prev_rewards_tree_cid: CID | None - prev_acc_shares: Iterable[RewardsTreeLeaf] - prev_strikes_tree_root: HexBytes - prev_strikes_tree_cid: CID | None - prev_acc_strikes: dict[StrikesValidator, StrikesList] + last_report: LastReport curr_distribution: Mock curr_rewards_tree_root: HexBytes curr_rewards_tree_cid: CID | Literal[""] @@ -390,12 +386,14 @@ class BuildReportTestParam: [ pytest.param( BuildReportTestParam( - prev_rewards_tree_root=HexBytes(ZERO_HASH), - prev_rewards_tree_cid=None, - prev_acc_shares=[], - prev_strikes_tree_root=HexBytes(ZERO_HASH), - prev_strikes_tree_cid=None, - prev_acc_strikes={}, + last_report=Mock( + rewards_tree_root=HexBytes(ZERO_HASH), + rewards_tree_cid=None, + rewards=[], + strikes_tree_root=HexBytes(ZERO_HASH), + strikes_tree_cid=None, + strikes={}, + ), curr_distribution=Mock( return_value=( # distributed @@ -429,12 +427,14 @@ class BuildReportTestParam: ), pytest.param( BuildReportTestParam( - prev_rewards_tree_root=HexBytes(ZERO_HASH), - prev_rewards_tree_cid=None, - prev_acc_shares=[], - prev_strikes_tree_root=HexBytes(ZERO_HASH), - prev_strikes_tree_cid=None, - prev_acc_strikes={}, + last_report=Mock( + rewards_tree_root=HexBytes(ZERO_HASH), + rewards_tree_cid=None, + rewards=[], + strikes_tree_root=HexBytes(ZERO_HASH), + strikes_tree_cid=None, + strikes={}, + ), curr_distribution=Mock( return_value=( # distributed @@ -470,18 +470,28 @@ class BuildReportTestParam: ), pytest.param( BuildReportTestParam( - prev_rewards_tree_root=HexBytes("OLD_TREE_ROOT".encode()), - prev_rewards_tree_cid=CID("QmOLD_TREE"), - prev_acc_shares=[(NodeOperatorId(0), 100), (NodeOperatorId(1), 200), (NodeOperatorId(2), 300)], - prev_strikes_tree_root=HexBytes(ZERO_HASH), - prev_strikes_tree_cid=None, - prev_acc_strikes={}, + last_report=Mock( + rewards_tree_root=HexBytes("OLD_TREE_ROOT".encode()), + rewards_tree_cid=CID("QmOLD_TREE"), + rewards=[(NodeOperatorId(0), 100), (NodeOperatorId(1), 200), (NodeOperatorId(2), 300)], + strikes_tree_root=HexBytes(ZERO_HASH), + strikes_tree_cid=None, + strikes={}, + ), curr_distribution=Mock( return_value=( # distributed 6, # shares - defaultdict(int, {NodeOperatorId(0): 1, NodeOperatorId(1): 2, NodeOperatorId(3): 3}), + defaultdict( + int, + { + NodeOperatorId(0): 101, + NodeOperatorId(1): 202, + NodeOperatorId(2): 300, + NodeOperatorId(3): 3, + }, + ), # strikes defaultdict(dict), # logs @@ -511,12 +521,14 @@ class BuildReportTestParam: ), pytest.param( BuildReportTestParam( - prev_rewards_tree_root=HexBytes("OLD_TREE_ROOT".encode()), - prev_rewards_tree_cid=CID("QmOLD_TREE"), - prev_acc_shares=[(NodeOperatorId(0), 100), (NodeOperatorId(1), 200), (NodeOperatorId(2), 300)], - prev_strikes_tree_root=HexBytes(ZERO_HASH), - prev_strikes_tree_cid=None, - prev_acc_strikes={}, + last_report=Mock( + rewards_tree_root=HexBytes("OLD_TREE_ROOT".encode()), + rewards_tree_cid=CID("QmOLD_TREE"), + rewards=[(NodeOperatorId(0), 100), (NodeOperatorId(1), 200), (NodeOperatorId(2), 300)], + strikes_tree_root=HexBytes(ZERO_HASH), + strikes_tree_cid=None, + strikes={}, + ), curr_distribution=Mock( return_value=( # distributed @@ -550,25 +562,25 @@ class BuildReportTestParam: ), ], ) -def test_build_report(csm: CSM, module: CSOracle, param: BuildReportTestParam): +@pytest.mark.usefixtures("csm") +def test_build_report(module: CSOracle, param: BuildReportTestParam): module.validate_state = Mock() module.report_contract.get_consensus_version = Mock(return_value=1) - # mock previous report - module.w3.csm.get_rewards_tree_root = Mock(return_value=param.prev_rewards_tree_root) - module.w3.csm.get_rewards_tree_cid = Mock(return_value=param.prev_rewards_tree_cid) - module.w3.csm.get_strikes_tree_root = Mock(return_value=param.prev_strikes_tree_root) - module.w3.csm.get_strikes_tree_cid = Mock(return_value=param.prev_strikes_tree_cid) - module.get_accumulated_rewards = Mock(return_value=param.prev_acc_shares) - module.get_accumulated_strikes = Mock(return_value=param.prev_acc_strikes) + module._get_last_report = Mock(return_value=param.last_report) # mock current frame module.calculate_distribution = param.curr_distribution - module._merge_strikes = Mock() module.make_rewards_tree = Mock(return_value=Mock(root=param.curr_rewards_tree_root)) module.make_strikes_tree = Mock(return_value=Mock(root=param.curr_strikes_tree_root)) - module.publish_tree = Mock(side_effect=[param.curr_rewards_tree_cid, param.curr_strikes_tree_cid]) + module.publish_tree = Mock( + side_effect=[ + param.curr_rewards_tree_cid, + param.curr_strikes_tree_cid, + ] + ) module.publish_log = Mock(return_value=param.curr_log_cid) - report = module.build_report(blockstamp=Mock(ref_slot=100500)) + blockstamp = Mock(ref_slot=100500) + report = module.build_report(blockstamp) assert module.make_rewards_tree.call_args == param.expected_make_rewards_tree_call_args assert report == param.expected_func_result @@ -605,7 +617,7 @@ def test_execute_module_processed(module: CSOracle): @pytest.fixture() -def tree(): +def rewards_tree(): return RewardsTree.new( [ (NodeOperatorId(0), 0), @@ -616,20 +628,40 @@ def tree(): ) -def test_get_accumulated_shares(module: CSOracle, tree: Tree): - encoded_tree = tree.encode() - module.w3.ipfs = Mock(fetch=Mock(return_value=encoded_tree)) +def test_last_report_rewards(web3: Web3, rewards_tree: RewardsTree): + encoded_tree = rewards_tree.encode() + web3.ipfs = Mock(fetch=Mock(return_value=encoded_tree)) - for i, leaf in enumerate(module.get_accumulated_rewards(cid=CIDv0("0x100500"), root=tree.root)): - assert tuple(leaf) == tree.tree.values[i]["value"] + last_report = LastReport( + w3=web3, + blockstamp=Mock(), + rewards_tree_root=rewards_tree.root, + strikes_tree_root=Mock(), + rewards_tree_cid=CID("QmRT"), + strikes_tree_cid=Mock(), + ) + for value in rewards_tree.values: + assert value in last_report.rewards -def test_get_accumulated_shares_unexpected_root(module: CSOracle, tree: Tree): - encoded_tree = tree.encode() - module.w3.ipfs = Mock(fetch=Mock(return_value=encoded_tree)) + web3.ipfs.fetch.assert_called_once_with(last_report.rewards_tree_cid) - with pytest.raises(ValueError): - next(module.get_accumulated_rewards(cid=CIDv0("0x100500"), root=HexBytes("0x100500"))) + +def test_get_accumulated_shares_unexpected_root(web3: Web3, rewards_tree: RewardsTree): + encoded_tree = rewards_tree.encode() + web3.ipfs = Mock(fetch=Mock(return_value=encoded_tree)) + + last_report = LastReport( + w3=web3, + blockstamp=Mock(), + rewards_tree_root=HexBytes("DOES NOT MATCH".encode()), + strikes_tree_root=Mock(), + rewards_tree_cid=CID("QmRT"), + strikes_tree_cid=Mock(), + ) + + with pytest.raises(ValueError, match="tree root"): + last_report.rewards @dataclass(frozen=True) @@ -722,7 +754,6 @@ def test_make_strikes_tree_negative(module: CSOracle, param: StrikesTreeTestPara @pytest.mark.parametrize( "param", [ - pytest.param(StrikesTreeTestParam(strikes={}, expected_tree_values=ValueError), id="empty"), pytest.param( StrikesTreeTestParam( strikes={ @@ -771,9 +802,5 @@ def test_make_strikes_tree_negative(module: CSOracle, param: StrikesTreeTestPara def test_make_strikes_tree(module: CSOracle, param: StrikesTreeTestParam): module.w3.csm.module.MAX_OPERATORS_COUNT = UINT64_MAX - if param.expected_tree_values is ValueError: - with pytest.raises(ValueError): - module.make_strikes_tree(param.strikes) - else: - tree = module.make_strikes_tree(param.strikes) - assert tree.values == param.expected_tree_values + tree = module.make_strikes_tree(param.strikes) + assert tree.values == param.expected_tree_values From 4cbee0fab00e9091d953a812faa9f6a43b4591fc Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Wed, 26 Feb 2025 22:01:28 +0100 Subject: [PATCH 078/162] chore: fix linter warnings --- tests/factory/no_registry.py | 1 - tests/modules/accounting/test_accounting_module.py | 2 +- .../accounting/test_safe_border_integration.py | 2 +- tests/modules/csm/test_csm_distribution.py | 2 +- tests/modules/csm/test_csm_module.py | 10 +++++----- tests/modules/ejector/test_ejector.py | 12 +++++------- tests/modules/ejector/test_sweep.py | 4 ++-- tests/modules/submodules/test_oracle_module.py | 1 - tests/utils/test_dataclass.py | 2 +- 9 files changed, 16 insertions(+), 20 deletions(-) diff --git a/tests/factory/no_registry.py b/tests/factory/no_registry.py index 1937bc218..ec55d3650 100644 --- a/tests/factory/no_registry.py +++ b/tests/factory/no_registry.py @@ -12,7 +12,6 @@ ETH1_ADDRESS_WITHDRAWAL_PREFIX, FAR_FUTURE_EPOCH, MAX_EFFECTIVE_BALANCE, - MIN_ACTIVATION_BALANCE, ) from src.providers.consensus.types import PendingDeposit, Validator, ValidatorState from src.providers.keys.types import LidoKey diff --git a/tests/modules/accounting/test_accounting_module.py b/tests/modules/accounting/test_accounting_module.py index b86023394..773ebe367 100644 --- a/tests/modules/accounting/test_accounting_module.py +++ b/tests/modules/accounting/test_accounting_module.py @@ -519,7 +519,7 @@ def test_accounting_get_processing_state_no_yet_init_epoch(accounting: Accountin assert isinstance(processing_state, AccountingProcessingState) assert processing_state.current_frame_ref_slot == 100 assert processing_state.processing_deadline_time == 200 - assert processing_state.main_data_submitted == False + assert processing_state.main_data_submitted is False assert processing_state.main_data_hash == ZERO_HASH diff --git a/tests/modules/accounting/test_safe_border_integration.py b/tests/modules/accounting/test_safe_border_integration.py index d50145e23..17b56c4df 100644 --- a/tests/modules/accounting/test_safe_border_integration.py +++ b/tests/modules/accounting/test_safe_border_integration.py @@ -1,4 +1,4 @@ -from unittest.mock import MagicMock, patch, Mock +from unittest.mock import MagicMock, Mock import pytest diff --git a/tests/modules/csm/test_csm_distribution.py b/tests/modules/csm/test_csm_distribution.py index 15000eae5..7a60a22ca 100644 --- a/tests/modules/csm/test_csm_distribution.py +++ b/tests/modules/csm/test_csm_distribution.py @@ -5,7 +5,7 @@ from web3.types import Wei from src.constants import UINT64_MAX -from src.modules.csm.csm import CSMError, CSOracle, LastReport +from src.modules.csm.csm import CSMError, CSOracle from src.modules.csm.log import FramePerfLog, ValidatorFrameSummary from src.modules.csm.state import AttestationsAccumulator, State from src.types import NodeOperatorId, ValidatorIndex diff --git a/tests/modules/csm/test_csm_module.py b/tests/modules/csm/test_csm_module.py index c7936a852..9c2eac7f0 100644 --- a/tests/modules/csm/test_csm_module.py +++ b/tests/modules/csm/test_csm_module.py @@ -1,20 +1,20 @@ import logging from collections import defaultdict from dataclasses import dataclass -from typing import Iterable, Literal, NoReturn, Type +from typing import Literal, NoReturn, Type from unittest.mock import Mock, PropertyMock, patch import pytest from hexbytes import HexBytes from src.constants import UINT64_MAX -from src.modules.csm.csm import CSOracle, LastReport, StrikesValidator +from src.modules.csm.csm import CSOracle, LastReport from src.modules.csm.state import State -from src.modules.csm.tree import RewardsTree, Tree -from src.modules.csm.types import RewardsTreeLeaf, StrikesList +from src.modules.csm.tree import RewardsTree +from src.modules.csm.types import StrikesList from src.modules.submodules.oracle_module import ModuleExecuteDelay from src.modules.submodules.types import ZERO_HASH, CurrentFrame -from src.providers.ipfs import CID, CIDv0 +from src.providers.ipfs import CID from src.types import NodeOperatorId, SlotNumber from src.web3py.extensions.csm import CSM from src.web3py.types import Web3 diff --git a/tests/modules/ejector/test_ejector.py b/tests/modules/ejector/test_ejector.py index 5e28c7483..d8685e3c5 100644 --- a/tests/modules/ejector/test_ejector.py +++ b/tests/modules/ejector/test_ejector.py @@ -535,13 +535,11 @@ def test_get_churn_limit_validators_less_than_min_churn( self, ejector: Ejector, ref_blockstamp: ReferenceBlockStamp, - monkeypatch: pytest.MonkeyPatch, ) -> None: - with monkeypatch.context() as m: - ejector.w3.cc.get_validators = Mock(return_value=[1, 1, 0]) - result = ejector._get_churn_limit(ref_blockstamp) - assert result == 4, "Unexpected churn limit" - ejector.w3.cc.get_validators.assert_called_once_with(ref_blockstamp) + ejector.w3.cc.get_validators = Mock(return_value=[1, 1, 0]) + result = ejector._get_churn_limit(ref_blockstamp) + assert result == 4, "Unexpected churn limit" + ejector.w3.cc.get_validators.assert_called_once_with(ref_blockstamp) @pytest.mark.unit @pytest.mark.usefixtures("consensus_client") @@ -596,7 +594,7 @@ def test_ejector_get_processing_state_no_yet_init_epoch(ejector: Ejector): assert isinstance(processing_state, EjectorProcessingState) assert processing_state.current_frame_ref_slot == 100 assert processing_state.processing_deadline_time == 200 - assert processing_state.data_submitted == False + assert processing_state.data_submitted is False def test_ejector_get_processing_state(ejector: Ejector): diff --git a/tests/modules/ejector/test_sweep.py b/tests/modules/ejector/test_sweep.py index 9419b5745..db4cca03f 100644 --- a/tests/modules/ejector/test_sweep.py +++ b/tests/modules/ejector/test_sweep.py @@ -1,10 +1,10 @@ import math -from unittest.mock import MagicMock, Mock +from unittest.mock import Mock import pytest import src.modules.ejector.sweep as sweep_module -from src.constants import ETH1_ADDRESS_WITHDRAWAL_PREFIX, MAX_WITHDRAWALS_PER_PAYLOAD, MIN_ACTIVATION_BALANCE +from src.constants import MAX_WITHDRAWALS_PER_PAYLOAD, MIN_ACTIVATION_BALANCE from src.modules.ejector.sweep import ( Withdrawal, get_pending_partial_withdrawals, diff --git a/tests/modules/submodules/test_oracle_module.py b/tests/modules/submodules/test_oracle_module.py index 0eaabc791..02fbe7114 100644 --- a/tests/modules/submodules/test_oracle_module.py +++ b/tests/modules/submodules/test_oracle_module.py @@ -1,5 +1,4 @@ from unittest.mock import Mock, patch, MagicMock -from typing import Type import pytest from requests.exceptions import ConnectionError as RequestsConnectionError diff --git a/tests/utils/test_dataclass.py b/tests/utils/test_dataclass.py index 61548dab0..62f64ca80 100644 --- a/tests/utils/test_dataclass.py +++ b/tests/utils/test_dataclass.py @@ -153,7 +153,7 @@ def test_dataclass_ignore_extra_fields(): def test_dataclass_raises_missing_field(): response = {"name": "Bob"} with pytest.raises(TypeError, match="age"): - pet = Pet.from_response(**response) + Pet.from_response(**response) @dataclass From ff60ce74ea9a2843f11469e4687195af4d761c25 Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Thu, 27 Feb 2025 09:37:49 +0100 Subject: [PATCH 079/162] test: cover LastReport class --- tests/modules/csm/test_csm_module.py | 199 ++++++++++++++++++++------- tests/modules/csm/test_tree.py | 10 +- 2 files changed, 155 insertions(+), 54 deletions(-) diff --git a/tests/modules/csm/test_csm_module.py b/tests/modules/csm/test_csm_module.py index 9c2eac7f0..f571cb94c 100644 --- a/tests/modules/csm/test_csm_module.py +++ b/tests/modules/csm/test_csm_module.py @@ -10,12 +10,13 @@ from src.constants import UINT64_MAX from src.modules.csm.csm import CSOracle, LastReport from src.modules.csm.state import State -from src.modules.csm.tree import RewardsTree +from src.modules.csm.tree import RewardsTree, StrikesTree from src.modules.csm.types import StrikesList from src.modules.submodules.oracle_module import ModuleExecuteDelay from src.modules.submodules.types import ZERO_HASH, CurrentFrame from src.providers.ipfs import CID from src.types import NodeOperatorId, SlotNumber +from src.utils.types import hex_str_to_bytes from src.web3py.extensions.csm import CSM from src.web3py.types import Web3 from tests.factory.blockstamp import ReferenceBlockStampFactory @@ -616,54 +617,6 @@ def test_execute_module_processed(module: CSOracle): assert execute_delay is ModuleExecuteDelay.NEXT_SLOT -@pytest.fixture() -def rewards_tree(): - return RewardsTree.new( - [ - (NodeOperatorId(0), 0), - (NodeOperatorId(1), 1), - (NodeOperatorId(2), 42), - (NodeOperatorId(UINT64_MAX), 0), - ] - ) - - -def test_last_report_rewards(web3: Web3, rewards_tree: RewardsTree): - encoded_tree = rewards_tree.encode() - web3.ipfs = Mock(fetch=Mock(return_value=encoded_tree)) - - last_report = LastReport( - w3=web3, - blockstamp=Mock(), - rewards_tree_root=rewards_tree.root, - strikes_tree_root=Mock(), - rewards_tree_cid=CID("QmRT"), - strikes_tree_cid=Mock(), - ) - - for value in rewards_tree.values: - assert value in last_report.rewards - - web3.ipfs.fetch.assert_called_once_with(last_report.rewards_tree_cid) - - -def test_get_accumulated_shares_unexpected_root(web3: Web3, rewards_tree: RewardsTree): - encoded_tree = rewards_tree.encode() - web3.ipfs = Mock(fetch=Mock(return_value=encoded_tree)) - - last_report = LastReport( - w3=web3, - blockstamp=Mock(), - rewards_tree_root=HexBytes("DOES NOT MATCH".encode()), - strikes_tree_root=Mock(), - rewards_tree_cid=CID("QmRT"), - strikes_tree_cid=Mock(), - ) - - with pytest.raises(ValueError, match="tree root"): - last_report.rewards - - @dataclass(frozen=True) class RewardsTreeTestParam: shares: dict[NodeOperatorId, int] @@ -804,3 +757,151 @@ def test_make_strikes_tree(module: CSOracle, param: StrikesTreeTestParam): tree = module.make_strikes_tree(param.strikes) assert tree.values == param.expected_tree_values + + +class TestLastReport: + @pytest.mark.usefixtures("csm") + def test_load(self, web3: Web3): + blockstamp = Mock() + + web3.csm.get_rewards_tree_root = Mock(return_value=HexBytes(b"42")) + web3.csm.get_rewards_tree_cid = Mock(return_value=CID("QmRT")) + web3.csm.get_strikes_tree_root = Mock(return_value=HexBytes(b"17")) + web3.csm.get_strikes_tree_cid = Mock(return_value=CID("QmST")) + + last_report = LastReport.load(web3, blockstamp) + + web3.csm.get_rewards_tree_root.assert_called_once_with(blockstamp) + web3.csm.get_rewards_tree_cid.assert_called_once_with(blockstamp) + web3.csm.get_strikes_tree_root.assert_called_once_with(blockstamp) + web3.csm.get_strikes_tree_cid.assert_called_once_with(blockstamp) + + assert last_report.rewards_tree_root == HexBytes(b"42") + assert last_report.rewards_tree_cid == CID("QmRT") + assert last_report.strikes_tree_root == HexBytes(b"17") + assert last_report.strikes_tree_cid == CID("QmST") + + def test_get_rewards_empty(self, web3: Web3): + web3.ipfs = Mock(fetch=Mock()) + + last_report = LastReport( + w3=web3, + blockstamp=Mock(), + rewards_tree_root=HexBytes(ZERO_HASH), + strikes_tree_root=Mock(), + rewards_tree_cid=None, + strikes_tree_cid=Mock(), + ) + + assert last_report.rewards == [] + web3.ipfs.fetch.assert_not_called() + + def test_get_rewards_okay(self, web3: Web3, rewards_tree: RewardsTree): + encoded_tree = rewards_tree.encode() + web3.ipfs = Mock(fetch=Mock(return_value=encoded_tree)) + + last_report = LastReport( + w3=web3, + blockstamp=Mock(), + rewards_tree_root=rewards_tree.root, + strikes_tree_root=Mock(), + rewards_tree_cid=CID("QmRT"), + strikes_tree_cid=Mock(), + ) + + for value in rewards_tree.values: + assert value in last_report.rewards + + web3.ipfs.fetch.assert_called_once_with(last_report.rewards_tree_cid) + + def test_get_rewards_unexpected_root(self, web3: Web3, rewards_tree: RewardsTree): + encoded_tree = rewards_tree.encode() + web3.ipfs = Mock(fetch=Mock(return_value=encoded_tree)) + + last_report = LastReport( + w3=web3, + blockstamp=Mock(), + rewards_tree_root=HexBytes("DOES NOT MATCH".encode()), + strikes_tree_root=Mock(), + rewards_tree_cid=CID("QmRT"), + strikes_tree_cid=Mock(), + ) + + with pytest.raises(ValueError, match="tree root"): + last_report.rewards + + web3.ipfs.fetch.assert_called_once_with(last_report.rewards_tree_cid) + + def test_get_strikes_empty(self, web3: Web3): + web3.ipfs = Mock(fetch=Mock()) + + last_report = LastReport( + w3=web3, + blockstamp=Mock(), + rewards_tree_root=Mock(), + strikes_tree_root=HexBytes(ZERO_HASH), + rewards_tree_cid=Mock(), + strikes_tree_cid=None, + ) + + assert last_report.strikes == {} + web3.ipfs.fetch.assert_not_called() + + def test_get_strikes_okay(self, web3: Web3, strikes_tree: StrikesTree): + encoded_tree = strikes_tree.encode() + web3.ipfs = Mock(fetch=Mock(return_value=encoded_tree)) + + last_report = LastReport( + w3=web3, + blockstamp=Mock(), + rewards_tree_root=Mock(), + strikes_tree_root=strikes_tree.root, + rewards_tree_cid=Mock(), + strikes_tree_cid=CID("QmST"), + ) + + for no_id, pubkey, value in strikes_tree.values: + assert last_report.strikes[(no_id, pubkey)] == value + + web3.ipfs.fetch.assert_called_once_with(last_report.strikes_tree_cid) + + def test_get_strikes_unexpected_root(self, web3: Web3, strikes_tree: StrikesTree): + encoded_tree = strikes_tree.encode() + web3.ipfs = Mock(fetch=Mock(return_value=encoded_tree)) + + last_report = LastReport( + w3=web3, + blockstamp=Mock(), + rewards_tree_root=Mock(), + strikes_tree_root=HexBytes("DOES NOT MATCH".encode()), + rewards_tree_cid=Mock(), + strikes_tree_cid=CID("QmRT"), + ) + + with pytest.raises(ValueError, match="tree root"): + last_report.strikes + + web3.ipfs.fetch.assert_called_once_with(last_report.strikes_tree_cid) + + @pytest.fixture() + def rewards_tree(self) -> RewardsTree: + return RewardsTree.new( + [ + (NodeOperatorId(0), 0), + (NodeOperatorId(1), 1), + (NodeOperatorId(2), 42), + (NodeOperatorId(UINT64_MAX), 0), + ] + ) + + @pytest.fixture() + def strikes_tree(self) -> StrikesTree: + return StrikesTree.new( + [ + (NodeOperatorId(0), HexBytes(hex_str_to_bytes("0x00")), StrikesList([0])), + (NodeOperatorId(1), HexBytes(hex_str_to_bytes("0x01")), StrikesList([1])), + (NodeOperatorId(1), HexBytes(hex_str_to_bytes("0x02")), StrikesList([1])), + (NodeOperatorId(2), HexBytes(hex_str_to_bytes("0x03")), StrikesList([1])), + (NodeOperatorId(UINT64_MAX), HexBytes(hex_str_to_bytes("0x64")), StrikesList([1, 0, 1])), + ] + ) diff --git a/tests/modules/csm/test_tree.py b/tests/modules/csm/test_tree.py index fee2dd197..8f8136546 100644 --- a/tests/modules/csm/test_tree.py +++ b/tests/modules/csm/test_tree.py @@ -70,11 +70,11 @@ class TestStrikesTree(TreeTestBase[StrikesTreeLeaf]): @property def values(self) -> list[StrikesTreeLeaf]: return [ - (NodeOperatorId(0), hex_str_to_bytes("0x00"), StrikesList([0])), - (NodeOperatorId(1), hex_str_to_bytes("0x01"), StrikesList([1])), - (NodeOperatorId(1), hex_str_to_bytes("0x02"), StrikesList([1])), - (NodeOperatorId(2), hex_str_to_bytes("0x03"), StrikesList([1])), - (NodeOperatorId(UINT64_MAX), hex_str_to_bytes("0x64"), StrikesList([1, 0, 1])), + (NodeOperatorId(0), HexBytes(hex_str_to_bytes("0x00")), StrikesList([0])), + (NodeOperatorId(1), HexBytes(hex_str_to_bytes("0x01")), StrikesList([1])), + (NodeOperatorId(1), HexBytes(hex_str_to_bytes("0x02")), StrikesList([1])), + (NodeOperatorId(2), HexBytes(hex_str_to_bytes("0x03")), StrikesList([1])), + (NodeOperatorId(UINT64_MAX), HexBytes(hex_str_to_bytes("0x64")), StrikesList([1, 0, 1])), ] def test_decoded_types(self, tree: StrikesTree) -> None: From cf87cc0edc003cf1f8834947f91b3840bc6e03c8 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Thu, 27 Feb 2025 10:34:16 +0100 Subject: [PATCH 080/162] fix: test logs --- tests/modules/csm/test_log.py | 99 ++++++++++++++++++++--------------- 1 file changed, 57 insertions(+), 42 deletions(-) diff --git a/tests/modules/csm/test_log.py b/tests/modules/csm/test_log.py index e7ea747cb..36f0ff861 100644 --- a/tests/modules/csm/test_log.py +++ b/tests/modules/csm/test_log.py @@ -2,6 +2,7 @@ import pytest from src.modules.csm.log import FramePerfLog, DutyAccumulator +from src.providers.execution.contracts.cs_parameters_registry import PerformanceCoefficients from src.types import EpochNumber, NodeOperatorId, ReferenceBlockStamp from tests.factory.blockstamp import ReferenceBlockStampFactory @@ -25,62 +26,76 @@ def test_fields_access(log: FramePerfLog): log.operators[NodeOperatorId(42)].validators["100500"].slashed = True -def test_log_encode(log: FramePerfLog): +def test_logs_encode(log: FramePerfLog): # Fill in dynamic fields to make sure we have data in it to be encoded. - log.operators[NodeOperatorId(42)].validators["41337"].attestation_duty = DutyAccumulator(220, 119) log.operators[NodeOperatorId(42)].distributed = 17 - log.operators[NodeOperatorId(0)].distributed = 0 - - logs = [log] + log.operators[NodeOperatorId(42)].performance_coefficients = PerformanceCoefficients() + log.operators[NodeOperatorId(42)].validators["41337"].attestation_duty = DutyAccumulator(220, 119) + log.operators[NodeOperatorId(42)].validators["41337"].proposal_duty = DutyAccumulator(1, 1) + log.operators[NodeOperatorId(42)].validators["41337"].sync_duty = DutyAccumulator(100500, 100000) + log.operators[NodeOperatorId(42)].validators["41337"].performance = 0.5 + log.operators[NodeOperatorId(42)].validators["41337"].threshold = 0.7 + log.operators[NodeOperatorId(42)].validators["41337"].rewards_share = 0.3 - encoded = FramePerfLog.encode(logs) + log.operators[NodeOperatorId(0)].distributed = 0 + log.operators[NodeOperatorId(0)].performance_coefficients = PerformanceCoefficients(1, 2, 3) - for decoded in json.loads(encoded): - assert decoded["operators"]["42"]["validators"]["41337"]["attestation_duty"]["assigned"] == 220 - assert decoded["operators"]["42"]["validators"]["41337"]["attestation_duty"]["included"] == 119 - assert decoded["operators"]["42"]["distributed"] == 17 - assert decoded["operators"]["0"]["distributed"] == 0 + log.distributable = 100 + log.distributed_rewards = 50 + log.rebate_to_protocol = 10 - assert decoded["blockstamp"]["block_hash"] == log.blockstamp.block_hash - assert decoded["blockstamp"]["ref_slot"] == log.blockstamp.ref_slot + log_2 = FramePerfLog(ReferenceBlockStampFactory.build(), (EpochNumber(500), EpochNumber(900))) + log_2.operators = log.operators - assert decoded["threshold"] == log.threshold - assert decoded["frame"] == list(log.frame) + log_2.distributable = 100000000 + log_2.distributed_rewards = 0 + log_2.rebate_to_protocol = 0 + logs = [log, log_2] -def test_logs_encode(): - log_0 = FramePerfLog(ReferenceBlockStampFactory.build(), (EpochNumber(100), EpochNumber(500))) - log_0.operators[NodeOperatorId(42)].validators["41337"].attestation_duty = DutyAccumulator(220, 119) - log_0.operators[NodeOperatorId(42)].distributed = 17 - log_0.operators[NodeOperatorId(0)].distributed = 0 + encoded = FramePerfLog.encode(logs) - log_1 = FramePerfLog(ReferenceBlockStampFactory.build(), (EpochNumber(500), EpochNumber(900))) - log_1.operators[NodeOperatorId(5)].validators["1234"].attestation_duty = DutyAccumulator(400, 399) - log_1.operators[NodeOperatorId(5)].distributed = 40 - log_1.operators[NodeOperatorId(18)].distributed = 0 + decoded_logs = json.loads(encoded) - logs = [log_0, log_1] + for decoded in decoded_logs: + assert decoded["operators"]["42"]["validators"]["41337"]["attestation_duty"]["assigned"] == 220 + assert decoded["operators"]["42"]["validators"]["41337"]["attestation_duty"]["included"] == 119 + assert decoded["operators"]["42"]["validators"]["41337"]["proposal_duty"]["assigned"] == 1 + assert decoded["operators"]["42"]["validators"]["41337"]["proposal_duty"]["included"] == 1 + assert decoded["operators"]["42"]["validators"]["41337"]["sync_duty"]["assigned"] == 100500 + assert decoded["operators"]["42"]["validators"]["41337"]["sync_duty"]["included"] == 100000 + assert decoded["operators"]["42"]["validators"]["41337"]["performance"] == 0.5 + assert decoded["operators"]["42"]["validators"]["41337"]["threshold"] == 0.7 + assert decoded["operators"]["42"]["validators"]["41337"]["rewards_share"] == 0.3 + assert decoded["operators"]["42"]["validators"]["41337"]["slashed"] == False + assert decoded["operators"]["42"]["distributed"] == 17 + assert decoded["operators"]["42"]["performance_coefficients"] == { + 'attestations_weight': 54, + 'blocks_weight': 8, + 'sync_weight': 2, + } - encoded = FramePerfLog.encode(logs) + assert decoded["operators"]["0"]["distributed"] == 0 + assert decoded["operators"]["0"]["performance_coefficients"] == { + 'attestations_weight': 1, + 'blocks_weight': 2, + 'sync_weight': 3, + } - decoded = json.loads(encoded) + assert decoded_logs[0]["blockstamp"]["block_hash"] == log.blockstamp.block_hash + assert decoded_logs[0]["blockstamp"]["ref_slot"] == log.blockstamp.ref_slot - assert len(decoded) == 2 + assert decoded_logs[0]["frame"] == list(log.frame) - assert decoded[0]["operators"]["42"]["validators"]["41337"]["attestation_duty"]["assigned"] == 220 - assert decoded[0]["operators"]["42"]["validators"]["41337"]["attestation_duty"]["included"] == 119 - assert decoded[0]["operators"]["42"]["distributed"] == 17 - assert decoded[0]["operators"]["0"]["distributed"] == 0 + assert decoded_logs[0]["distributable"] == log.distributable + assert decoded_logs[0]["distributed_rewards"] == log.distributed_rewards + assert decoded_logs[0]["rebate_to_protocol"] == log.rebate_to_protocol - assert decoded[1]["operators"]["5"]["validators"]["1234"]["attestation_duty"]["assigned"] == 400 - assert decoded[1]["operators"]["5"]["validators"]["1234"]["attestation_duty"]["included"] == 399 - assert decoded[1]["operators"]["5"]["distributed"] == 40 - assert decoded[1]["operators"]["18"]["distributed"] == 0 + assert decoded_logs[1]["blockstamp"]["block_hash"] == log_2.blockstamp.block_hash + assert decoded_logs[1]["blockstamp"]["ref_slot"] == log_2.blockstamp.ref_slot - for i, log in enumerate(logs): - assert decoded[i]["blockstamp"]["block_hash"] == log.blockstamp.block_hash - assert decoded[i]["blockstamp"]["ref_slot"] == log.blockstamp.ref_slot + assert decoded_logs[1]["frame"] == list(log_2.frame) - assert decoded[i]["threshold"] == log.threshold - assert decoded[i]["frame"] == list(log.frame) - assert decoded[i]["distributable"] == log.distributable + assert decoded_logs[1]["distributable"] == log_2.distributable + assert decoded_logs[1]["distributed_rewards"] == log_2.distributed_rewards + assert decoded_logs[1]["rebate_to_protocol"] == log_2.rebate_to_protocol From 7f3ae45ca4dc08888e77f6928ad5e6f08eb974f8 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Thu, 27 Feb 2025 10:35:47 +0100 Subject: [PATCH 081/162] fix: test_get_block_attestations_and_sync --- tests/providers/consensus/test_consensus_client.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/providers/consensus/test_consensus_client.py b/tests/providers/consensus/test_consensus_client.py index 06955dc2e..3fba752ab 100644 --- a/tests/providers/consensus/test_consensus_client.py +++ b/tests/providers/consensus/test_consensus_client.py @@ -32,11 +32,14 @@ def test_get_block_details(consensus_client: ConsensusClient, web3): @pytest.mark.integration -def test_get_block_attestations(consensus_client: ConsensusClient): +def test_get_block_attestations_and_sync(consensus_client: ConsensusClient): root = consensus_client.get_block_root('finalized').root - attestations = list(consensus_client.get_block_attestations(root)) + attestations_and_syncs = consensus_client.get_block_attestations_and_sync(root) + assert attestations_and_syncs + attestations, syncs = attestations_and_syncs assert attestations + assert syncs @pytest.mark.integration From 45490e0914de20c664a438225fd9940c169b323f Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Thu, 27 Feb 2025 10:52:34 +0100 Subject: [PATCH 082/162] fix: test_build_report --- src/modules/csm/csm.py | 8 +++- tests/modules/csm/test_csm_module.py | 65 +++++++++++++++------------- 2 files changed, 42 insertions(+), 31 deletions(-) diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index 99fa24e84..22e33c46c 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -95,8 +95,7 @@ def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: if (prev_cid is None) != (prev_root == ZERO_HASH): raise InconsistentData(f"Got inconsistent previous tree data: {prev_root=} {prev_cid=}") - distribution = Distribution(self.w3, self.converter(blockstamp), self.state) - distribution.calculate(blockstamp) + distribution = self.calculate_distribution(blockstamp) logs_cid = self.publish_log(distribution.logs) @@ -136,6 +135,11 @@ def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: strikes_tree_cid="", ).as_tuple() + def calculate_distribution(self, blockstamp: ReferenceBlockStamp) -> Distribution: + distribution = Distribution(self.w3, self.converter(blockstamp), self.state) + distribution.calculate(blockstamp) + return distribution + def is_main_data_submitted(self, blockstamp: BlockStamp) -> bool: last_ref_slot = self.w3.csm.get_csm_last_processing_ref_slot(blockstamp) ref_slot = self.get_initial_or_current_frame(blockstamp).ref_slot diff --git a/tests/modules/csm/test_csm_module.py b/tests/modules/csm/test_csm_module.py index 4e039f8f2..3ae0cca43 100644 --- a/tests/modules/csm/test_csm_module.py +++ b/tests/modules/csm/test_csm_module.py @@ -9,6 +9,7 @@ from src.constants import UINT64_MAX from src.modules.csm.csm import CSOracle +from src.modules.csm.distribution import Distribution from src.modules.csm.state import State from src.modules.csm.tree import Tree from src.modules.submodules.oracle_module import ModuleExecuteDelay @@ -382,20 +383,19 @@ class BuildReportTestParam: prev_tree_cid=None, prev_acc_shares=[], curr_distribution=Mock( - return_value=( - # distributed - 0, - # shares - defaultdict(int), - # log - Mock(), + return_value=Mock( + spec=Distribution, + total_rewards=0, + total_rewards_map=defaultdict(int), + total_rebate=0, + logs=[Mock()], ) ), curr_tree_root=HexBytes(ZERO_HASH), curr_tree_cid="", curr_log_cid=CID("QmLOG"), expected_make_tree_call_args=None, - expected_func_result=(1, 100500, HexBytes(ZERO_HASH), "", CID("QmLOG"), 0), + expected_func_result=(1, 100500, HexBytes(ZERO_HASH), "", CID("QmLOG"), 0, 0, HexBytes(ZERO_HASH), ""), ), id="empty_prev_report_and_no_new_distribution", ), @@ -405,13 +405,12 @@ class BuildReportTestParam: prev_tree_cid=None, prev_acc_shares=[], curr_distribution=Mock( - return_value=( - # distributed - 6, - # shares - defaultdict(int, {NodeOperatorId(0): 1, NodeOperatorId(1): 2, NodeOperatorId(2): 3}), - # log - Mock(), + return_value=Mock( + spec=Distribution, + total_rewards=6, + total_rewards_map=defaultdict(int, {NodeOperatorId(0): 1, NodeOperatorId(1): 2, NodeOperatorId(2): 3}), + total_rebate=1, + logs=[Mock()], ) ), curr_tree_root=HexBytes("NEW_TREE_ROOT".encode()), @@ -425,6 +424,9 @@ class BuildReportTestParam: CID("QmNEW_TREE"), CID("QmLOG"), 6, + 1, + HexBytes(ZERO_HASH), + "", ), ), id="empty_prev_report_and_new_distribution", @@ -435,13 +437,13 @@ class BuildReportTestParam: prev_tree_cid=CID("QmOLD_TREE"), prev_acc_shares=[(NodeOperatorId(0), 100), (NodeOperatorId(1), 200), (NodeOperatorId(2), 300)], curr_distribution=Mock( - return_value=( - # distributed - 6, - # shares - defaultdict(int, {NodeOperatorId(0): 1, NodeOperatorId(1): 2, NodeOperatorId(3): 3}), - # log - Mock(), + return_value=Mock( + spec=Distribution, + total_rewards=6, + total_rewards_map=defaultdict(int, {NodeOperatorId(0): 1, NodeOperatorId(1): 2, + NodeOperatorId(3): 3}), + total_rebate=1, + logs=[Mock()], ) ), curr_tree_root=HexBytes("NEW_TREE_ROOT".encode()), @@ -457,6 +459,9 @@ class BuildReportTestParam: CID("QmNEW_TREE"), CID("QmLOG"), 6, + 1, + HexBytes(ZERO_HASH), + "", ), ), id="non_empty_prev_report_and_new_distribution", @@ -467,13 +472,12 @@ class BuildReportTestParam: prev_tree_cid=CID("QmOLD_TREE"), prev_acc_shares=[(NodeOperatorId(0), 100), (NodeOperatorId(1), 200), (NodeOperatorId(2), 300)], curr_distribution=Mock( - return_value=( - # distributed - 0, - # shares - defaultdict(int), - # log - Mock(), + return_value=Mock( + spec=Distribution, + total_rewards=0, + total_rewards_map=defaultdict(int), + total_rebate=0, + logs=[Mock()], ) ), curr_tree_root=HexBytes(32), @@ -487,6 +491,9 @@ class BuildReportTestParam: CID("QmOLD_TREE"), CID("QmLOG"), 0, + 0, + HexBytes(ZERO_HASH), + "", ), ), id="non_empty_prev_report_and_no_new_distribution", From 0f3a0f84300e5acc16d832470dd20e809403f1a0 Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Thu, 27 Feb 2025 11:07:43 +0100 Subject: [PATCH 083/162] wip: tests for csm distribution --- tests/modules/csm/test_csm_distribution.py | 148 +++++++++++++++++++-- 1 file changed, 136 insertions(+), 12 deletions(-) diff --git a/tests/modules/csm/test_csm_distribution.py b/tests/modules/csm/test_csm_distribution.py index 7a60a22ca..85cd53b5a 100644 --- a/tests/modules/csm/test_csm_distribution.py +++ b/tests/modules/csm/test_csm_distribution.py @@ -8,6 +8,8 @@ from src.modules.csm.csm import CSMError, CSOracle from src.modules.csm.log import FramePerfLog, ValidatorFrameSummary from src.modules.csm.state import AttestationsAccumulator, State +from src.modules.csm.types import StrikesList +from src.providers.execution.contracts.cs_parameters_registry import StrikesParams from src.types import NodeOperatorId, ValidatorIndex from src.web3py.extensions import CSM from tests.factory.blockstamp import ReferenceBlockStampFactory @@ -27,13 +29,25 @@ def module(web3, csm: CSM): def test_calculate_distribution_handles_single_frame(module: CSOracle): module.state = Mock() module.state.frames = [(1, 2)] - blockstamp = Mock() + blockstamp = ReferenceBlockStampFactory.build(ref_epoch=2) last_report = Mock(strikes={}, rewards=[]) module.module_validators_by_node_operators = Mock() - module._get_ref_blockstamp_for_frame = Mock(return_value=blockstamp) module._get_performance_threshold = Mock(return_value=1) - module.w3.csm.fee_distributor.shares_to_distribute = Mock(return_value=500) - module._calculate_distribution_in_frame = Mock(return_value=({NodeOperatorId(1): 500}, {})) + module.w3.csm.fee_distributor.shares_to_distribute = Mock(side_effect=[500]) + module.w3.csm.get_strikes_params = Mock(return_value=StrikesParams(lifetime=6, threshold=Mock())) + module._calculate_distribution_in_frame = Mock( + return_value=( + # rewards + { + NodeOperatorId(1): 500, + }, + # strikes + { + (NodeOperatorId(0), b"42"): 1, + (NodeOperatorId(2), b"17"): 2, + }, + ) + ) ( total_distributed, @@ -44,8 +58,11 @@ def test_calculate_distribution_handles_single_frame(module: CSOracle): assert total_distributed == 500 assert total_rewards[NodeOperatorId(1)] == 500 + assert strikes == { + (NodeOperatorId(0), b"42"): [1, 0, 0, 0, 0, 0], + (NodeOperatorId(2), b"17"): [2, 0, 0, 0, 0, 0], + } assert len(logs) == 1 - assert not strikes def test_calculate_distribution_handles_multiple_frames(module: CSOracle): @@ -56,12 +73,43 @@ def test_calculate_distribution_handles_multiple_frames(module: CSOracle): module.module_validators_by_node_operators = Mock() module._get_ref_blockstamp_for_frame = Mock(return_value=blockstamp) module._get_performance_threshold = Mock(return_value=1) - module.w3.csm.fee_distributor.shares_to_distribute = Mock(return_value=800) + module.w3.csm.fee_distributor.shares_to_distribute = Mock(side_effect=[500, 1500, 1600]) + module.w3.csm.get_strikes_params = Mock(return_value=StrikesParams(lifetime=6, threshold=Mock())) module._calculate_distribution_in_frame = Mock( side_effect=[ - ({NodeOperatorId(1): 500}, {}), - ({NodeOperatorId(1): 136}, {}), - ({NodeOperatorId(1): 164}, {}), + ( + # rewards + { + NodeOperatorId(1): 500, + }, + # strikes + { + (NodeOperatorId(0), b"42"): 1, + (NodeOperatorId(2), b"17"): 2, + }, + ), + ( + # rewards + { + NodeOperatorId(1): 136, + NodeOperatorId(3): 777, + }, + # strikes + { + (NodeOperatorId(0), b"42"): 3, + }, + ), + ( + # rewards + { + NodeOperatorId(1): 164, + }, + # strikes + { + (NodeOperatorId(2), b"17"): 4, + (NodeOperatorId(2), b"18"): 1, + }, + ), ] ) @@ -72,10 +120,15 @@ def test_calculate_distribution_handles_multiple_frames(module: CSOracle): logs, ) = module.calculate_distribution(blockstamp, last_report) - assert total_distributed == 800 + assert total_distributed == 800 + 777 assert total_rewards[NodeOperatorId(1)] == 800 - assert len(logs) == 3 - assert not strikes + assert total_rewards[NodeOperatorId(3)] == 777 + assert strikes == { + (NodeOperatorId(0), b"42"): [0, 3, 1, 0, 0, 0], + (NodeOperatorId(2), b"17"): [4, 0, 2, 0, 0, 0], + (NodeOperatorId(2), b"18"): [1, 0, 0, 0, 0, 0], + } + assert len(logs) == len(module.state.frames) module._get_ref_blockstamp_for_frame.assert_has_calls( [call(blockstamp, frame[1]) for frame in module.state.frames[1:]] ) @@ -331,3 +384,74 @@ def test_calc_rewards_distribution_in_frame_handles_negative_to_distribute(): with pytest.raises(ValueError, match="Invalid rewards to distribute"): CSOracle.calc_rewards_distribution_in_frame(participation_shares, rewards_to_distribute) + + +@pytest.mark.parametrize( + ("acc", "strikes_in_frame", "threshold_per_op", "expected"), + [ + pytest.param({}, {}, {}, {}, id="empty_acc_empty_strikes_in_frame"), + pytest.param( + {}, + { + (NodeOperatorId(42), b"00"): 3, + (NodeOperatorId(17), b"01"): 1, + }, + { + NodeOperatorId(42): Mock(lifetime=6), + NodeOperatorId(17): Mock(lifetime=4), + }, + { + (NodeOperatorId(42), b"00"): [3, 0, 0, 0, 0, 0], + (NodeOperatorId(17), b"01"): [1, 0, 0, 0], + }, + id="empty_acc_non_empty_strikes_in_frame", + ), + pytest.param( + { + (NodeOperatorId(42), b"00"): StrikesList([3, 0, 0, 0, 0, 0]), + (NodeOperatorId(17), b"01"): StrikesList([1, 0, 0, 0]), + }, + {}, + { + NodeOperatorId(42): Mock(lifetime=5), + NodeOperatorId(17): Mock(lifetime=4), + }, + { + (NodeOperatorId(42), b"00"): [0, 3, 0, 0, 0], + (NodeOperatorId(17), b"01"): [0, 1, 0, 0], + }, + id="non_empty_acc_empty_strikes_in_frame", + ), + pytest.param( + { + (NodeOperatorId(42), b"00"): StrikesList([3, 0, 0, 0, 0, 0]), + (NodeOperatorId(17), b"01"): StrikesList([1, 0, 0, 0]), + }, + { + (NodeOperatorId(42), b"00"): 2, + (NodeOperatorId(18), b"02"): 1, + }, + { + NodeOperatorId(42): Mock(lifetime=5), + NodeOperatorId(17): Mock(lifetime=4), + NodeOperatorId(18): Mock(lifetime=6), + }, + { + (NodeOperatorId(42), b"00"): [2, 3, 0, 0, 0], + (NodeOperatorId(17), b"01"): [0, 1, 0, 0], + (NodeOperatorId(18), b"02"): [1, 0, 0, 0, 0, 0], + }, + id="non_empty_acc_non_empty_strikes_in_frame", + ), + ], +) +def test_merge_strikes( + module: CSOracle, + acc: dict, + strikes_in_frame: dict, + threshold_per_op: dict, + expected: dict, +): + module.w3.csm.get_strikes_params = Mock(side_effect=lambda no_id, _: threshold_per_op[no_id]) + module._merge_strikes(acc, strikes_in_frame, frame_blockstamp=Mock()) + assert acc == expected From 3b11503e04ac6b5e859eccd02cf7f4458e2dfc9d Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Thu, 27 Feb 2025 13:23:02 +0100 Subject: [PATCH 084/162] remove: redundant cache --- src/web3py/extensions/csm.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/web3py/extensions/csm.py b/src/web3py/extensions/csm.py index 484df8cf9..3ec248cdf 100644 --- a/src/web3py/extensions/csm.py +++ b/src/web3py/extensions/csm.py @@ -19,7 +19,6 @@ from src.providers.execution.contracts.cs_strikes import CSStrikesContract from src.providers.ipfs import CID, CIDv0, CIDv1, is_cid_v0 from src.types import BlockStamp, NodeOperatorId, SlotNumber -from src.utils.cache import global_lru_cache as lru_cache logger = logging.getLogger(__name__) @@ -61,7 +60,6 @@ def get_strikes_tree_cid(self, blockstamp: BlockStamp) -> CID | None: return None return CIDv0(result) if is_cid_v0(result) else CIDv1(result) - @lru_cache def get_strikes_params(self, no_id: NodeOperatorId, blockstamp: BlockStamp) -> StrikesParams: curve_id = self.accounting.get_bond_curve_id(no_id, blockstamp.block_hash) return self.params.get_strikes_params(curve_id, blockstamp.block_hash) From 364e679b5f8794ef690c6a1231108aa5a8e7d782 Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Thu, 27 Feb 2025 13:24:09 +0100 Subject: [PATCH 085/162] chore: do not modify last_report.strikes value by an accident --- src/modules/csm/csm.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index 2ece93fbe..a41e4bccc 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -232,8 +232,9 @@ def calculate_distribution( total_rewards = defaultdict[NodeOperatorId, Shares](Shares) total_distributed = Shares(0) - strikes = last_report.strikes logs: list[FramePerfLog] = [] + strikes: dict[StrikesValidator, StrikesList] = {} + strikes.update(last_report.strikes.items()) for frame in self.state.frames: from_epoch, to_epoch = frame From cf2e935d63ca35685d65bf873a19a40e87cb6cfb Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Thu, 27 Feb 2025 13:25:57 +0100 Subject: [PATCH 086/162] chore: refactor rewards merge --- src/modules/csm/csm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index a41e4bccc..e1ff31369 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -277,8 +277,8 @@ def calculate_distribution( if total_distributed != sum(total_rewards.values()): raise InconsistentData(f"Invalid distribution: {sum(total_rewards.values())=} != {total_distributed=}") - for no_id, rewards in last_report.rewards: - total_rewards[no_id] += rewards + for no_id, last_report_rewards in last_report.rewards: + total_rewards[no_id] += last_report_rewards return total_distributed, total_rewards, strikes, logs From 574147a384f07a7258507667d50a5187b3e80930 Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Thu, 27 Feb 2025 13:26:16 +0100 Subject: [PATCH 087/162] fix: wipe strikes if got empty dict --- src/modules/csm/csm.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index e1ff31369..9ea291073 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -118,6 +118,9 @@ def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: strikes_tree = self.make_strikes_tree(strikes) strikes_tree_root = strikes_tree.root strikes_cid = self.publish_tree(strikes_tree) + else: + strikes_tree_root = HexBytes(ZERO_HASH) + strikes_cid = None logs_cid = self.publish_log(logs) From 0aff5fff529dd50ad2fa26655f99d5186274f30d Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Thu, 27 Feb 2025 13:26:44 +0100 Subject: [PATCH 088/162] chore: sanity checks in LastReport --- src/modules/csm/csm.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index 9ea291073..9c785cf98 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -549,6 +549,12 @@ def load(cls, w3: Web3, blockstamp: BlockStamp) -> Self: @cached_property def rewards(self) -> Iterable[RewardsTreeLeaf]: + if (self.rewards_tree_cid is None) != (self.rewards_tree_root == ZERO_HASH): + raise InconsistentData( + "Got inconsistent previous rewards tree data: " + f"tree_root={self.rewards_tree_root.hex()} tree_cid={self.rewards_tree_cid=}" + ) + if self.rewards_tree_cid is None or self.rewards_tree_root == ZERO_HASH: logger.info({"msg": f"No rewards distribution as of {self.blockstamp=}."}) return [] @@ -565,6 +571,12 @@ def rewards(self) -> Iterable[RewardsTreeLeaf]: @cached_property def strikes(self) -> dict[StrikesValidator, StrikesList]: + if (self.strikes_tree_cid is None) != (self.strikes_tree_root == ZERO_HASH): + raise InconsistentData( + "Got inconsistent previous strikes tree data: " + f"tree_root={self.strikes_tree_root.hex()} tree_cid={self.strikes_tree_cid=}" + ) + if self.strikes_tree_cid is None or self.strikes_tree_root == ZERO_HASH: logger.info({"msg": f"No strikes reported as of {self.blockstamp=}."}) return {} From 7ec12f03b068968ae1869c383df98940cfbdf60f Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Thu, 27 Feb 2025 14:32:44 +0100 Subject: [PATCH 089/162] chore: linter --- tests/modules/csm/test_csm_distribution.py | 4 +++- tests/modules/csm/test_csm_module.py | 10 +++++++--- tests/modules/csm/test_state.py | 8 ++------ 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/tests/modules/csm/test_csm_distribution.py b/tests/modules/csm/test_csm_distribution.py index fdf2ce4ae..f2df64415 100644 --- a/tests/modules/csm/test_csm_distribution.py +++ b/tests/modules/csm/test_csm_distribution.py @@ -269,7 +269,9 @@ def test_process_validator_duty_handles_slashed_validator(): prop_duty = DutyAccumulator(assigned=1, included=1) sync_duty = DutyAccumulator(assigned=1, included=1) - CSOracle.process_validator_duty(validator, attestation_duty, prop_duty, sync_duty, threshold, participation_shares, log_operator) + CSOracle.process_validator_duty( + validator, attestation_duty, prop_duty, sync_duty, threshold, participation_shares, log_operator + ) assert participation_shares[validator.lido_id.operatorIndex] == 0 assert log_operator.validators[validator.index].slashed is True diff --git a/tests/modules/csm/test_csm_module.py b/tests/modules/csm/test_csm_module.py index 3ae0cca43..d9d7a9385 100644 --- a/tests/modules/csm/test_csm_module.py +++ b/tests/modules/csm/test_csm_module.py @@ -34,6 +34,7 @@ def module(web3, csm: CSM): def test_init(module: CSOracle): assert module + # Static functions you were dreaming of for so long. @@ -408,7 +409,9 @@ class BuildReportTestParam: return_value=Mock( spec=Distribution, total_rewards=6, - total_rewards_map=defaultdict(int, {NodeOperatorId(0): 1, NodeOperatorId(1): 2, NodeOperatorId(2): 3}), + total_rewards_map=defaultdict( + int, {NodeOperatorId(0): 1, NodeOperatorId(1): 2, NodeOperatorId(2): 3} + ), total_rebate=1, logs=[Mock()], ) @@ -440,8 +443,9 @@ class BuildReportTestParam: return_value=Mock( spec=Distribution, total_rewards=6, - total_rewards_map=defaultdict(int, {NodeOperatorId(0): 1, NodeOperatorId(1): 2, - NodeOperatorId(3): 3}), + total_rewards_map=defaultdict( + int, {NodeOperatorId(0): 1, NodeOperatorId(1): 2, NodeOperatorId(3): 3} + ), total_rebate=1, logs=[Mock()], ) diff --git a/tests/modules/csm/test_state.py b/tests/modules/csm/test_state.py index 10a01d776..c7d2a2015 100644 --- a/tests/modules/csm/test_state.py +++ b/tests/modules/csm/test_state.py @@ -654,18 +654,14 @@ def test_get_att_network_aggr_raises_error_for_invalid_accumulator(): def test_get_prop_network_aggr_raises_error_for_invalid_accumulator(): state = State() - state.data = { - (0, 31): Duties(proposals=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 15)})) - } + state.data = {(0, 31): Duties(proposals=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 15)}))} with pytest.raises(ValueError, match="Invalid accumulator"): state.get_prop_network_aggr((0, 31)) def test_get_sync_network_aggr_raises_error_for_invalid_accumulator(): state = State() - state.data = { - (0, 31): Duties(syncs=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 15)})) - } + state.data = {(0, 31): Duties(syncs=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 15)}))} with pytest.raises(ValueError, match="Invalid accumulator"): state.get_sync_network_aggr((0, 31)) From 62f2741e4d0ef526bb088acf8fafc01483a1cf10 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Thu, 27 Feb 2025 17:01:02 +0100 Subject: [PATCH 090/162] chore: merge with strikes --- src/modules/csm/csm.py | 106 ++----------- src/modules/csm/distribution.py | 143 ++++++++++++------ src/modules/csm/helpers/__init__.py | 0 src/modules/csm/helpers/last_report.py | 94 ++++++++++++ src/modules/csm/types.py | 2 + .../execution/contracts/cs_accounting.py | 2 +- .../contracts/cs_parameters_registry.py | 13 +- src/web3py/extensions/csm.py | 12 +- 8 files changed, 225 insertions(+), 147 deletions(-) create mode 100644 src/modules/csm/helpers/__init__.py create mode 100644 src/modules/csm/helpers/last_report.py diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index 37d234349..16e2866dd 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -1,8 +1,4 @@ import logging -from collections import defaultdict -from dataclasses import dataclass -from functools import cached_property -from typing import Iterable, Self from hexbytes import HexBytes @@ -14,11 +10,12 @@ ) from src.metrics.prometheus.duration_meter import duration_meter from src.modules.csm.checkpoint import FrameCheckpointProcessor, FrameCheckpointsIterator, MinStepIsNotReached -from src.modules.csm.distribution import Distribution -from src.modules.csm.log import FramePerfLog, OperatorFrameSummary -from src.modules.csm.state import Frame, State +from src.modules.csm.distribution import Distribution, StrikesValidator +from src.modules.csm.helpers.last_report import LastReport +from src.modules.csm.log import FramePerfLog +from src.modules.csm.state import State from src.modules.csm.tree import RewardsTree, StrikesTree, Tree -from src.modules.csm.types import ReportData, RewardsTreeLeaf, Shares, StrikesList +from src.modules.csm.types import ReportData, Shares, StrikesList from src.modules.submodules.consensus import ConsensusModule from src.modules.submodules.oracle_module import BaseModule, ModuleExecuteDelay from src.modules.submodules.types import ZERO_HASH @@ -47,9 +44,6 @@ class CSMError(Exception): """Unrecoverable error in CSM module""" -type StrikesValidator = tuple[NodeOperatorId, HexBytes] - - class CSOracle(BaseModule, ConsensusModule): """ CSM performance module collects performance of CSM node operators and creates a Merkle tree of the resulting @@ -97,7 +91,6 @@ def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: last_report = self._get_last_report(blockstamp) rewards_tree_root, rewards_cid = last_report.rewards_tree_root, last_report.rewards_tree_cid - strikes_tree_root, strikes_cid = last_report.strikes_tree_root, last_report.strikes_tree_cid distribution = self.calculate_distribution(blockstamp, last_report) @@ -110,11 +103,15 @@ def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: strikes_tree = self.make_strikes_tree(distribution.strikes) strikes_tree_root = strikes_tree.root strikes_cid = self.publish_tree(strikes_tree) + if strikes_tree_root == last_report.strikes_tree_root: + logger.warning({"msg": "Strikes tree root is the same as the previous one"}) + if strikes_cid == last_report.strikes_tree_cid: + logger.warning({"msg": "Strikes tree CID is the same as the previous one"}) else: strikes_tree_root = HexBytes(ZERO_HASH) strikes_cid = None - logs_cid = self.publish_log(logs) + logs_cid = self.publish_log(distribution.logs) return ReportData( self.get_consensus_version(blockstamp), @@ -128,7 +125,10 @@ def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: strikes_tree_cid=strikes_cid or "", ).as_tuple() - def calculate_distribution(self, blockstamp: ReferenceBlockStamp, last_report: "LastReport") -> Distribution: + def _get_last_report(self, blockstamp: BlockStamp) -> LastReport: + return LastReport.load(self.w3, blockstamp) + + def calculate_distribution(self, blockstamp: ReferenceBlockStamp, last_report: LastReport) -> Distribution: distribution = Distribution(self.w3, self.converter(blockstamp), self.state) distribution.calculate(blockstamp, last_report) return distribution @@ -306,81 +306,3 @@ def get_epochs_range_to_process(self, blockstamp: BlockStamp) -> tuple[EpochNumb def converter(self, blockstamp: BlockStamp) -> Web3Converter: return Web3Converter(self.get_chain_config(blockstamp), self.get_frame_config(blockstamp)) - - -@dataclass -class LastReport: - w3: Web3 - blockstamp: BlockStamp - - rewards_tree_root: HexBytes - strikes_tree_root: HexBytes - rewards_tree_cid: CID | None - strikes_tree_cid: CID | None - - @classmethod - def load(cls, w3: Web3, blockstamp: BlockStamp) -> Self: - rewards_tree_root = w3.csm.get_rewards_tree_root(blockstamp) - rewards_tree_cid = w3.csm.get_rewards_tree_cid(blockstamp) - - if (rewards_tree_cid is None) != (rewards_tree_root == ZERO_HASH): - raise InconsistentData(f"Got inconsistent previous tree data: {rewards_tree_root=} {rewards_tree_cid=}") - - strikes_tree_root = w3.csm.get_strikes_tree_root(blockstamp) - strikes_tree_cid = w3.csm.get_strikes_tree_cid(blockstamp) - - if (strikes_tree_cid is None) != (strikes_tree_root == ZERO_HASH): - raise InconsistentData(f"Got inconsistent previous tree data: {strikes_tree_root=} {strikes_tree_cid=}") - - return cls( - w3, - blockstamp, - rewards_tree_root, - strikes_tree_root, - rewards_tree_cid, - strikes_tree_cid, - ) - - @cached_property - def rewards(self) -> Iterable[RewardsTreeLeaf]: - if (self.rewards_tree_cid is None) != (self.rewards_tree_root == ZERO_HASH): - raise InconsistentData( - "Got inconsistent previous rewards tree data: " - f"tree_root={self.rewards_tree_root.hex()} tree_cid={self.rewards_tree_cid=}" - ) - - if self.rewards_tree_cid is None or self.rewards_tree_root == ZERO_HASH: - logger.info({"msg": f"No rewards distribution as of {self.blockstamp=}."}) - return [] - - logger.info({"msg": "Fetching rewards tree by CID from IPFS", "cid": repr(self.rewards_tree_cid)}) - tree = RewardsTree.decode(self.w3.ipfs.fetch(self.rewards_tree_cid)) - - logger.info({"msg": "Restored rewards tree from IPFS dump", "root": repr(tree.root)}) - - if tree.root != self.rewards_tree_root: - raise ValueError("Unexpected rewards tree root got from IPFS dump") - - return tree.values - - @cached_property - def strikes(self) -> dict[StrikesValidator, StrikesList]: - if (self.strikes_tree_cid is None) != (self.strikes_tree_root == ZERO_HASH): - raise InconsistentData( - "Got inconsistent previous strikes tree data: " - f"tree_root={self.strikes_tree_root.hex()} tree_cid={self.strikes_tree_cid=}" - ) - - if self.strikes_tree_cid is None or self.strikes_tree_root == ZERO_HASH: - logger.info({"msg": f"No strikes reported as of {self.blockstamp=}."}) - return {} - - logger.info({"msg": "Fetching tree by CID from IPFS", "cid": repr(self.strikes_tree_cid)}) - tree = StrikesTree.decode(self.w3.ipfs.fetch(self.strikes_tree_cid)) - - logger.info({"msg": "Restored strikes tree from IPFS dump", "root": repr(tree.root)}) - - if tree.root != self.strikes_tree_root: - raise ValueError("Unexpected strikes tree root got from IPFS dump") - - return {(no_id, pubkey): strikes for no_id, pubkey, strikes in tree.values} diff --git a/src/modules/csm/distribution.py b/src/modules/csm/distribution.py index 45b873b11..d68f70b2b 100644 --- a/src/modules/csm/distribution.py +++ b/src/modules/csm/distribution.py @@ -1,11 +1,12 @@ import logging import math from collections import defaultdict -from functools import lru_cache +from dataclasses import dataclass +from src.modules.csm.helpers.last_report import LastReport from src.modules.csm.log import FramePerfLog, OperatorFrameSummary from src.modules.csm.state import DutyAccumulator, Frame, State -from src.modules.csm.types import Shares +from src.modules.csm.types import Shares, StrikesList, StrikesValidator from src.providers.execution.contracts.cs_parameters_registry import PerformanceCoefficients from src.providers.execution.exceptions import InconsistentData from src.types import NodeOperatorId, ReferenceBlockStamp, EpochNumber, StakingModuleAddress @@ -17,6 +18,19 @@ logger = logging.getLogger(__name__) +@dataclass +class ValidatorDuties: + attestation: DutyAccumulator | None + proposal: DutyAccumulator | None + sync: DutyAccumulator | None + + +@dataclass +class ValidatorDutyOutcome: + rebate_share: int + strikes: int + + class Distribution: w3: Web3 converter: Web3Converter @@ -25,6 +39,7 @@ class Distribution: total_rewards: Shares total_rewards_map: defaultdict[NodeOperatorId, Shares] total_rebate: Shares + strikes: dict[StrikesValidator, StrikesList] logs: list[FramePerfLog] def __init__(self, w3: Web3, converter: Web3Converter, state: State): @@ -35,13 +50,18 @@ def __init__(self, w3: Web3, converter: Web3Converter, state: State): self.total_rewards = 0 self.total_rewards_map = defaultdict[NodeOperatorId, int](int) self.total_rebate = 0 + self.strikes = {} self.logs: list[FramePerfLog] = [] - def calculate(self, blockstamp: ReferenceBlockStamp) -> None: + def calculate(self, blockstamp: ReferenceBlockStamp, last_report: LastReport) -> None: """Computes distribution of fee shares at the given timestamp""" + + self.strikes.update(last_report.strikes.items()) + for frame in self.state.frames: - logger.info({"msg": f"Calculating distribution for {frame=}"}) - _, to_epoch = frame + from_epoch, to_epoch = frame + logger.info({"msg": f"Calculating distribution for frame [{from_epoch};{to_epoch}]"}) + frame_blockstamp = self._get_frame_blockstamp(blockstamp, to_epoch) frame_module_validators = self._get_module_validators(frame_blockstamp) @@ -49,14 +69,21 @@ def calculate(self, blockstamp: ReferenceBlockStamp) -> None: distributed_so_far = self.total_rewards + self.total_rebate rewards_to_distribute_in_frame = total_rewards_to_distribute - distributed_so_far + frame_log = FramePerfLog(blockstamp, frame) ( rewards_map_in_frame, distributed_rewards_in_frame, rebate_to_protocol_in_frame, - frame_log + strikes_in_frame ) = self._calculate_distribution_in_frame( - frame, frame_blockstamp, rewards_to_distribute_in_frame, frame_module_validators + frame, frame_blockstamp, rewards_to_distribute_in_frame, frame_module_validators, frame_log ) + if not distributed_rewards_in_frame: + logger.info({"msg": f"No rewards distributed in frame [{from_epoch};{to_epoch}]"}) + + self._merge_strikes(self.strikes, strikes_in_frame, frame_blockstamp) + if not strikes_in_frame: + logger.info({"msg": f"No strikes in frame [{from_epoch};{to_epoch}]"}) self.total_rewards += distributed_rewards_in_frame self.total_rebate += rebate_to_protocol_in_frame @@ -71,6 +98,9 @@ def calculate(self, blockstamp: ReferenceBlockStamp) -> None: if self.total_rewards != sum(self.total_rewards_map.values()): raise InconsistentData(f"Invalid distribution: {sum(self.total_rewards_map.values())=} != {self.total_rewards=}") + for no_id, last_report_rewards in last_report.rewards: + self.total_rewards_map[no_id] += last_report_rewards + def _get_frame_blockstamp(self, blockstamp: ReferenceBlockStamp, to_epoch: EpochNumber) -> ReferenceBlockStamp: if to_epoch != blockstamp.ref_epoch: return self._get_ref_blockstamp_for_frame(blockstamp, to_epoch) @@ -97,10 +127,11 @@ def _calculate_distribution_in_frame( blockstamp: ReferenceBlockStamp, rewards_to_distribute: Shares, operators_to_validators: ValidatorsByNodeOperator, - ) -> tuple[dict[NodeOperatorId, Shares], Shares, Shares, FramePerfLog]: + log: FramePerfLog, + ) -> tuple[dict[NodeOperatorId, Shares], Shares, Shares, dict[StrikesValidator, int]]: total_rebate_share = 0 participation_shares: defaultdict[NodeOperatorId, Shares] = defaultdict(int) - log = FramePerfLog(blockstamp, frame) + frame_strikes: dict[StrikesValidator, int] = {} network_perf = self._get_network_performance(frame) @@ -113,54 +144,46 @@ def _calculate_distribution_in_frame( logger.info({"msg": f"Calculating distribution for {no_id=}"}) log_operator = log.operators[no_id] - curve_id = self.w3.csm.accounting.get_bond_curve_id(no_id, blockstamp.block_hash) - perf_coeffs, perf_leeway, reward_share = self._get_curve_params(curve_id, blockstamp) + curve_params = self.w3.csm.get_curve_params(no_id, blockstamp) sorted_active_validators = sorted(active_validators, key=lambda v: v.index) for key_number, validator in enumerate(sorted_active_validators): - key_threshold = max(network_perf - perf_leeway.get_for(key_number), 0) - key_reward_share = reward_share.get_for(key_number) + key_threshold = max(network_perf - curve_params.perf_leeway_data.get_for(key_number), 0) + key_reward_share = curve_params.reward_share_data.get_for(key_number) att_duty = self.state.data[frame].attestations.get(validator.index) prop_duty = self.state.data[frame].proposals.get(validator.index) sync_duty = self.state.data[frame].syncs.get(validator.index) - # TODO: better naming - validator_rebate = self.process_validator_duties( + duties = ValidatorDuties(att_duty, prop_duty, sync_duty) + + validator_duty_outcome = self.process_validator_duties( validator, - att_duty, - prop_duty, - sync_duty, + duties, key_threshold, key_reward_share, - perf_coeffs, + curve_params.perf_coeffs, participation_shares, log_operator, ) - total_rebate_share += validator_rebate + if validator_duty_outcome.strikes: + frame_strikes[(no_id, validator.pubkey)] = validator_duty_outcome.strikes + log_operator.validators[validator.index].strikes = validator_duty_outcome.strikes + total_rebate_share += validator_duty_outcome.rebate_share rewards_distribution = self.calc_rewards_distribution_in_frame( participation_shares, total_rebate_share, rewards_to_distribute ) - - for no_id, no_rewards in rewards_distribution.items(): - log.operators[no_id].distributed = no_rewards - distributed_rewards = sum(rewards_distribution.values()) rebate_to_protocol = rewards_to_distribute - distributed_rewards + for no_id, no_rewards in rewards_distribution.items(): + log.operators[no_id].distributed = no_rewards log.distributable = rewards_to_distribute log.distributed_rewards = distributed_rewards log.rebate_to_protocol = rebate_to_protocol - return rewards_distribution, distributed_rewards, rebate_to_protocol, log - - @lru_cache() - def _get_curve_params(self, curve_id: int, blockstamp: ReferenceBlockStamp): - perf_coeffs = self.w3.csm.params.get_performance_coefficients(curve_id, blockstamp.block_hash) - perf_leeway_data = self.w3.csm.params.get_performance_leeway_data(curve_id, blockstamp.block_hash) - reward_share_data = self.w3.csm.params.get_reward_share_data(curve_id, blockstamp.block_hash) - return perf_coeffs, perf_leeway_data, reward_share_data + return rewards_distribution, distributed_rewards, rebate_to_protocol, frame_strikes def _get_network_performance(self, frame: Frame) -> float: att_perf = self.state.get_att_network_aggr(frame) @@ -172,19 +195,17 @@ def _get_network_performance(self, frame: Frame) -> float: @staticmethod def process_validator_duties( validator: LidoValidator, - attestation: DutyAccumulator | None, - sync: DutyAccumulator | None, - proposal: DutyAccumulator | None, + duties: ValidatorDuties, threshold: float, reward_share: float, perf_coeffs: PerformanceCoefficients, participation_shares: defaultdict[NodeOperatorId, int], log_operator: OperatorFrameSummary, - ) -> int: - if attestation is None: + ) -> ValidatorDutyOutcome: + if duties.attestation is None: # It's possible that the validator is not assigned to any duty, hence it's performance # is not presented in the aggregates (e.g. exited, pending for activation etc). - return 0 + return ValidatorDutyOutcome(rebate_share=0, strikes=0) log_validator = log_operator.validators[validator.index] @@ -195,16 +216,16 @@ def process_validator_duties( # It means that validator was active during the frame and got slashed and didn't meet the exit # epoch, so we should not count such validator for operator's share. log_validator.slashed = True - return 0 + return ValidatorDutyOutcome(rebate_share=0, strikes=1) - performance = perf_coeffs.calc_performance(attestation, proposal, sync) + performance = perf_coeffs.calc_performance(duties.attestation, duties.proposal, duties.sync) log_validator.performance = performance - log_validator.attestation_duty = attestation - if proposal: - log_validator.proposal_duty = proposal - if sync: - log_validator.sync_duty = sync + log_validator.attestation_duty = duties.attestation + if duties.proposal: + log_validator.proposal_duty = duties.proposal + if duties.sync: + log_validator.sync_duty = duties.sync if performance > threshold: # @@ -219,12 +240,15 @@ def process_validator_duties( # 87.55 ≈ 88 of 103 participation shares should be counted for the operator key's reward. # The rest 15 participation shares should be counted for the protocol's rebate. # - participation_share = math.ceil(attestation.assigned * reward_share) + participation_share = math.ceil(duties.attestation.assigned * reward_share) participation_shares[validator.lido_id.operatorIndex] += participation_share - rebate_share = attestation.assigned - participation_share - return rebate_share + rebate_share = duties.attestation.assigned - participation_share + assert rebate_share >= 0, f"Invalid rebate share: {rebate_share=}" + return ValidatorDutyOutcome(rebate_share=rebate_share, strikes=0) - return 0 + # In case of bad performance the validator should be striked and assigned attestations are not counted for + # the operator's reward and rebate, so rewards will be socialized between CSM operators. + return ValidatorDutyOutcome(rebate_share=0, strikes=1) @staticmethod def calc_rewards_distribution_in_frame( @@ -249,3 +273,24 @@ def validate_distribution(total_distributed_rewards, total_rebate, total_rewards raise ValueError( f"Invalid distribution: {total_distributed_rewards + total_rebate} > {total_rewards_to_distribute}" ) + + def _merge_strikes( + self, + acc: dict[StrikesValidator, StrikesList], + strikes_in_frame: dict[StrikesValidator, int], + frame_blockstamp: ReferenceBlockStamp, + ) -> None: + for key in strikes_in_frame: + if key not in acc: + acc[key] = StrikesList() + acc[key].push(strikes_in_frame[key]) + + for key in acc: + no_id, _ = key + if key not in strikes_in_frame: + acc[key].push(StrikesList.SENTINEL) # Just shifting... + maxlen = self.w3.csm.get_curve_params(no_id, frame_blockstamp).strikes_params.lifetime + acc[key].resize(maxlen) + # NOTE: Cleanup sequences like [0,0,0] since they don't bring any information. + if not sum(acc[key]): + del acc[key] diff --git a/src/modules/csm/helpers/__init__.py b/src/modules/csm/helpers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/modules/csm/helpers/last_report.py b/src/modules/csm/helpers/last_report.py new file mode 100644 index 000000000..7c73d1ed1 --- /dev/null +++ b/src/modules/csm/helpers/last_report.py @@ -0,0 +1,94 @@ +import logging +from dataclasses import dataclass +from functools import cached_property +from typing import Self, Iterable + +from hexbytes import HexBytes + +from src.modules.csm.tree import RewardsTree, StrikesTree +from src.modules.submodules.types import ZERO_HASH +from src.providers.execution.exceptions import InconsistentData +from src.providers.ipfs import CID +from src.types import BlockStamp +from src.modules.csm.types import RewardsTreeLeaf, StrikesList, StrikesValidator +from src.web3py.types import Web3 + +logger = logging.getLogger(__name__) + + +@dataclass +class LastReport: + w3: Web3 + blockstamp: BlockStamp + + rewards_tree_root: HexBytes + strikes_tree_root: HexBytes + rewards_tree_cid: CID | None + strikes_tree_cid: CID | None + + @classmethod + def load(cls, w3: Web3, blockstamp: BlockStamp) -> Self: + rewards_tree_root = w3.csm.get_rewards_tree_root(blockstamp) + rewards_tree_cid = w3.csm.get_rewards_tree_cid(blockstamp) + + if (rewards_tree_cid is None) != (rewards_tree_root == ZERO_HASH): + raise InconsistentData(f"Got inconsistent previous tree data: {rewards_tree_root=} {rewards_tree_cid=}") + + strikes_tree_root = w3.csm.get_strikes_tree_root(blockstamp) + strikes_tree_cid = w3.csm.get_strikes_tree_cid(blockstamp) + + if (strikes_tree_cid is None) != (strikes_tree_root == ZERO_HASH): + raise InconsistentData(f"Got inconsistent previous tree data: {strikes_tree_root=} {strikes_tree_cid=}") + + return cls( + w3, + blockstamp, + rewards_tree_root, + strikes_tree_root, + rewards_tree_cid, + strikes_tree_cid, + ) + + @cached_property + def rewards(self) -> Iterable[RewardsTreeLeaf]: + if (self.rewards_tree_cid is None) != (self.rewards_tree_root == ZERO_HASH): + raise InconsistentData( + "Got inconsistent previous rewards tree data: " + f"tree_root={self.rewards_tree_root.hex()} tree_cid={self.rewards_tree_cid=}" + ) + + if self.rewards_tree_cid is None or self.rewards_tree_root == ZERO_HASH: + logger.info({"msg": f"No rewards distribution as of {self.blockstamp=}."}) + return [] + + logger.info({"msg": "Fetching rewards tree by CID from IPFS", "cid": repr(self.rewards_tree_cid)}) + tree = RewardsTree.decode(self.w3.ipfs.fetch(self.rewards_tree_cid)) + + logger.info({"msg": "Restored rewards tree from IPFS dump", "root": repr(tree.root)}) + + if tree.root != self.rewards_tree_root: + raise ValueError("Unexpected rewards tree root got from IPFS dump") + + return tree.values + + @cached_property + def strikes(self) -> dict[StrikesValidator, StrikesList]: + if (self.strikes_tree_cid is None) != (self.strikes_tree_root == ZERO_HASH): + raise InconsistentData( + "Got inconsistent previous strikes tree data: " + f"tree_root={self.strikes_tree_root.hex()} tree_cid={self.strikes_tree_cid=}" + ) + + if self.strikes_tree_cid is None or self.strikes_tree_root == ZERO_HASH: + logger.info({"msg": f"No strikes reported as of {self.blockstamp=}."}) + return {} + + logger.info({"msg": "Fetching tree by CID from IPFS", "cid": repr(self.strikes_tree_cid)}) + tree = StrikesTree.decode(self.w3.ipfs.fetch(self.strikes_tree_cid)) + + logger.info({"msg": "Restored strikes tree from IPFS dump", "root": repr(tree.root)}) + + if tree.root != self.strikes_tree_root: + raise ValueError("Unexpected strikes tree root got from IPFS dump") + + return {(no_id, pubkey): strikes for no_id, pubkey, strikes in tree.values} diff --git a/src/modules/csm/types.py b/src/modules/csm/types.py index 9be7d5abb..f7b9b0b4c 100644 --- a/src/modules/csm/types.py +++ b/src/modules/csm/types.py @@ -9,6 +9,8 @@ logger = logging.getLogger(__name__) +type StrikesValidator = tuple[NodeOperatorId, HexBytes] + class StrikesList(Sequence[int]): """Deque-like structure to store strikes""" diff --git a/src/providers/execution/contracts/cs_accounting.py b/src/providers/execution/contracts/cs_accounting.py index d7df82f83..7fc73f942 100644 --- a/src/providers/execution/contracts/cs_accounting.py +++ b/src/providers/execution/contracts/cs_accounting.py @@ -28,7 +28,7 @@ def fee_distributor(self, block_identifier: BlockIdentifier = "latest") -> Check ) return Web3.to_checksum_address(resp) - @lru_cache + @lru_cache() def get_bond_curve_id(self, node_operator_id: NodeOperatorId, block_identifier: BlockIdentifier = "latest") -> int: """Returns the curve ID""" diff --git a/src/providers/execution/contracts/cs_parameters_registry.py b/src/providers/execution/contracts/cs_parameters_registry.py index c36d34782..190eedcf0 100644 --- a/src/providers/execution/contracts/cs_parameters_registry.py +++ b/src/providers/execution/contracts/cs_parameters_registry.py @@ -66,9 +66,18 @@ class StrikesParams: threshold: int +@dataclass +class CurveParams: + perf_coeffs: PerformanceCoefficients + perf_leeway_data: PerformanceLeeway + reward_share_data: RewardShare + strikes_params: StrikesParams + + class CSParametersRegistryContract(ContractInterface): abi_path = "./assets/CSParametersRegistry.json" + @lru_cache() def get_performance_coefficients( self, curve_id: int, @@ -86,6 +95,7 @@ def get_performance_coefficients( ) return PerformanceCoefficients(*resp) + @lru_cache() def get_reward_share_data( self, curve_id: int, @@ -103,6 +113,7 @@ def get_reward_share_data( ) return RewardShare(*resp) + @lru_cache() def get_performance_leeway_data( self, curve_id: int, @@ -120,7 +131,7 @@ def get_performance_leeway_data( ) return PerformanceLeeway(*resp) - @lru_cache + @lru_cache() def get_strikes_params( self, curve_id: int, diff --git a/src/web3py/extensions/csm.py b/src/web3py/extensions/csm.py index 3ec248cdf..9202de1f2 100644 --- a/src/web3py/extensions/csm.py +++ b/src/web3py/extensions/csm.py @@ -15,7 +15,7 @@ from src.providers.execution.contracts.cs_fee_distributor import CSFeeDistributorContract from src.providers.execution.contracts.cs_fee_oracle import CSFeeOracleContract from src.providers.execution.contracts.cs_module import CSModuleContract -from src.providers.execution.contracts.cs_parameters_registry import CSParametersRegistryContract, StrikesParams +from src.providers.execution.contracts.cs_parameters_registry import CSParametersRegistryContract, CurveParams from src.providers.execution.contracts.cs_strikes import CSStrikesContract from src.providers.ipfs import CID, CIDv0, CIDv1, is_cid_v0 from src.types import BlockStamp, NodeOperatorId, SlotNumber @@ -60,9 +60,13 @@ def get_strikes_tree_cid(self, blockstamp: BlockStamp) -> CID | None: return None return CIDv0(result) if is_cid_v0(result) else CIDv1(result) - def get_strikes_params(self, no_id: NodeOperatorId, blockstamp: BlockStamp) -> StrikesParams: + def get_curve_params(self, no_id: NodeOperatorId, blockstamp: BlockStamp) -> CurveParams: curve_id = self.accounting.get_bond_curve_id(no_id, blockstamp.block_hash) - return self.params.get_strikes_params(curve_id, blockstamp.block_hash) + perf_coeffs = self.params.get_performance_coefficients(curve_id, blockstamp.block_hash) + perf_leeway_data = self.params.get_performance_leeway_data(curve_id, blockstamp.block_hash) + reward_share_data = self.params.get_reward_share_data(curve_id, blockstamp.block_hash) + strikes_params = self.params.get_strikes_params(curve_id, blockstamp.block_hash) + return CurveParams(perf_coeffs, perf_leeway_data, reward_share_data, strikes_params) def _load_contracts(self) -> None: try: @@ -115,7 +119,7 @@ def _load_contracts(self) -> None: CSStrikesContract, self.w3.eth.contract( address=self.oracle.strikes(), - ContractFactoryClass=CSFeeOracleContract, + ContractFactoryClass=CSStrikesContract, decode_tuples=True, ), ) From 9c8a0402fb7ebd6cf09fd56e96c6e63f6c19b86f Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Thu, 27 Feb 2025 17:55:55 +0100 Subject: [PATCH 091/162] chore: linter --- tests/modules/csm/test_csm_module.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/modules/csm/test_csm_module.py b/tests/modules/csm/test_csm_module.py index 1ff0b7665..464ec8a7b 100644 --- a/tests/modules/csm/test_csm_module.py +++ b/tests/modules/csm/test_csm_module.py @@ -480,7 +480,13 @@ class BuildReportTestParam: spec=Distribution, total_rewards=6, total_rewards_map=defaultdict( - int, {NodeOperatorId(0): 101, NodeOperatorId(1): 202, NodeOperatorId(2): 300, NodeOperatorId(3): 3} + int, + { + NodeOperatorId(0): 101, + NodeOperatorId(1): 202, + NodeOperatorId(2): 300, + NodeOperatorId(3): 3, + }, ), total_rebate=1, strikes=defaultdict(dict), From e8a1b7c1019432e3d92f23c05ceb05e84979a308 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Fri, 28 Feb 2025 10:16:49 +0100 Subject: [PATCH 092/162] fix: distribution tests as is --- src/modules/csm/distribution.py | 5 +- tests/modules/csm/test_csm_distribution.py | 496 +++++++++++++-------- 2 files changed, 314 insertions(+), 187 deletions(-) diff --git a/src/modules/csm/distribution.py b/src/modules/csm/distribution.py index d68f70b2b..fc9e1da7b 100644 --- a/src/modules/csm/distribution.py +++ b/src/modules/csm/distribution.py @@ -202,7 +202,7 @@ def process_validator_duties( participation_shares: defaultdict[NodeOperatorId, int], log_operator: OperatorFrameSummary, ) -> ValidatorDutyOutcome: - if duties.attestation is None: + if duties.attestation is None or duties.attestation.assigned == 0: # It's possible that the validator is not assigned to any duty, hence it's performance # is not presented in the aggregates (e.g. exited, pending for activation etc). return ValidatorDutyOutcome(rebate_share=0, strikes=0) @@ -256,6 +256,9 @@ def calc_rewards_distribution_in_frame( rebate_share: int, rewards_to_distribute: int, ) -> dict[NodeOperatorId, Shares]: + if rewards_to_distribute < 0: + raise ValueError(f"Invalid rewards to distribute: {rewards_to_distribute=}") + rewards_distribution: dict[NodeOperatorId, int] = defaultdict(int) total_shares = rebate_share + sum(participation_shares.values()) diff --git a/tests/modules/csm/test_csm_distribution.py b/tests/modules/csm/test_csm_distribution.py index 35c613f53..72171f282 100644 --- a/tests/modules/csm/test_csm_distribution.py +++ b/tests/modules/csm/test_csm_distribution.py @@ -1,16 +1,18 @@ from collections import defaultdict -from unittest.mock import Mock, call +from unittest.mock import Mock, call, patch import pytest from web3.types import Wei from src.constants import UINT64_MAX -from src.modules.csm.csm import CSMError, CSOracle +from src.modules.csm.csm import CSOracle +from src.modules.csm.distribution import Distribution, ValidatorDuties from src.modules.csm.log import FramePerfLog, ValidatorFrameSummary -from src.modules.csm.state import DutyAccumulator, State +from src.modules.csm.state import DutyAccumulator, State, Duties from src.modules.csm.types import StrikesList -from src.providers.execution.contracts.cs_parameters_registry import StrikesParams -from src.types import NodeOperatorId, ValidatorIndex +from src.providers.execution.contracts.cs_parameters_registry import StrikesParams, PerformanceCoefficients +from src.providers.execution.exceptions import InconsistentData +from src.types import NodeOperatorId from src.web3py.extensions import CSM from tests.factory.blockstamp import ReferenceBlockStampFactory from tests.factory.no_registry import LidoValidatorFactory @@ -21,145 +23,183 @@ def module(web3, csm: CSM): yield CSOracle(web3) -def test_calculate_distribution_handles_single_frame(module: CSOracle): +def test_calculate_distribution_handles_single_frame(module: CSOracle, monkeypatch): + module.converter = Mock() module.state = Mock() module.state.frames = [(1, 2)] blockstamp = ReferenceBlockStampFactory.build(ref_epoch=2) last_report = Mock(strikes={}, rewards=[]) - module.module_validators_by_node_operators = Mock() - module._get_performance_threshold = Mock(return_value=1) + module.w3.csm.fee_distributor.shares_to_distribute = Mock(side_effect=[500]) - module.w3.csm.get_strikes_params = Mock(return_value=StrikesParams(lifetime=6, threshold=Mock())) - module._calculate_distribution_in_frame = Mock( - return_value=( - # rewards - { - NodeOperatorId(1): 500, - }, - # strikes - { - (NodeOperatorId(0), b"42"): 1, - (NodeOperatorId(2), b"17"): 2, - }, - ) + module.w3.csm.get_curve_params = Mock(return_value=Mock(strikes_params=StrikesParams(lifetime=6, threshold=Mock()))) + module.w3.lido_validators = Mock(get_module_validators_by_node_operators=Mock(return_value={})) + + monkeypatch.setattr( + Distribution, + "_calculate_distribution_in_frame", + Mock( + return_value=( + # rewards + { + NodeOperatorId(1): 500, + }, + # distributed_rewards + 500, + # rebate_to_protocol + 0, + # strikes + { + (NodeOperatorId(0), b"42"): 1, + (NodeOperatorId(2), b"17"): 2, + }, + ) + ), ) - ( - total_distributed, - total_rewards, - strikes, - logs, - ) = module.calculate_distribution(blockstamp, last_report) + distribution = module.calculate_distribution(blockstamp, last_report) - assert total_distributed == 500 - assert total_rewards[NodeOperatorId(1)] == 500 - assert strikes == { + assert distribution.total_rewards == 500 + assert distribution.total_rewards_map[NodeOperatorId(1)] == 500 + assert distribution.strikes == { (NodeOperatorId(0), b"42"): [1, 0, 0, 0, 0, 0], (NodeOperatorId(2), b"17"): [2, 0, 0, 0, 0, 0], } - assert len(logs) == 1 + assert len(distribution.logs) == 1 -def test_calculate_distribution_handles_multiple_frames(module: CSOracle): +def test_calculate_distribution_handles_multiple_frames(module: CSOracle, monkeypatch): + module.converter = Mock() module.state = Mock() module.state.frames = [(1, 2), (3, 4), (5, 6)] blockstamp = ReferenceBlockStampFactory.build(ref_epoch=2) last_report = Mock(strikes={}, rewards=[]) - module.module_validators_by_node_operators = Mock() - module._get_ref_blockstamp_for_frame = Mock(return_value=blockstamp) - module._get_performance_threshold = Mock(return_value=1) + module.w3.csm.fee_distributor.shares_to_distribute = Mock(side_effect=[500, 1500, 1600]) - module.w3.csm.get_strikes_params = Mock(return_value=StrikesParams(lifetime=6, threshold=Mock())) - module._calculate_distribution_in_frame = Mock( - side_effect=[ - ( - # rewards - { - NodeOperatorId(1): 500, - }, - # strikes - { - (NodeOperatorId(0), b"42"): 1, - (NodeOperatorId(2), b"17"): 2, - }, - ), - ( - # rewards - { - NodeOperatorId(1): 136, - NodeOperatorId(3): 777, - }, - # strikes - { - (NodeOperatorId(0), b"42"): 3, - }, - ), - ( - # rewards - { - NodeOperatorId(1): 164, - }, - # strikes - { - (NodeOperatorId(2), b"17"): 4, - (NodeOperatorId(2), b"18"): 1, - }, - ), - ] + module.w3.csm.get_curve_params = Mock(return_value=Mock(strikes_params=StrikesParams(lifetime=6, threshold=Mock()))) + module.w3.lido_validators = Mock(get_module_validators_by_node_operators=Mock(return_value={})) + + monkeypatch.setattr(Distribution, "_get_ref_blockstamp_for_frame", Mock(return_value=blockstamp)) + monkeypatch.setattr( + Distribution, + "_calculate_distribution_in_frame", + Mock( + side_effect=[ + ( + # rewards + { + NodeOperatorId(1): 500, + }, + # distributed_rewards + 500, + # rebate_to_protocol + 0, + # strikes + { + (NodeOperatorId(0), b"42"): 1, + (NodeOperatorId(2), b"17"): 2, + }, + ), + ( + # rewards + { + NodeOperatorId(1): 136, + NodeOperatorId(3): 777, + }, + # distributed_rewards + 913, + # rebate_to_protocol + 0, + # strikes + { + (NodeOperatorId(0), b"42"): 3, + }, + ), + ( + # rewards + { + NodeOperatorId(1): 164, + }, + # distributed_rewards + 164, + # rebate_to_protocol + 0, + # strikes + { + (NodeOperatorId(2), b"17"): 4, + (NodeOperatorId(2), b"18"): 1, + }, + ), + ] + ), ) - ( - total_distributed, - total_rewards, - strikes, - logs, - ) = module.calculate_distribution(blockstamp, last_report) - - assert total_distributed == 800 + 777 - assert total_rewards[NodeOperatorId(1)] == 800 - assert total_rewards[NodeOperatorId(3)] == 777 - assert strikes == { + distribution = module.calculate_distribution(blockstamp, last_report) + + assert distribution.total_rewards == 800 + 777 + assert distribution.total_rewards_map[NodeOperatorId(1)] == 800 + assert distribution.total_rewards_map[NodeOperatorId(3)] == 777 + assert distribution.strikes == { (NodeOperatorId(0), b"42"): [0, 3, 1, 0, 0, 0], (NodeOperatorId(2), b"17"): [4, 0, 2, 0, 0, 0], (NodeOperatorId(2), b"18"): [1, 0, 0, 0, 0, 0], } - assert len(logs) == len(module.state.frames) - module._get_ref_blockstamp_for_frame.assert_has_calls( + assert len(distribution.logs) == len(module.state.frames) + Distribution._get_ref_blockstamp_for_frame.assert_has_calls( [call(blockstamp, frame[1]) for frame in module.state.frames[1:]] ) -def test_calculate_distribution_handles_invalid_distribution(module: CSOracle): +def test_calculate_distribution_handles_invalid_distribution(module: CSOracle, monkeypatch): + module.converter = Mock() module.state = Mock() module.state.frames = [(1, 2)] blockstamp = Mock() last_report = Mock(strikes={}, rewards=[]) - module.module_validators_by_node_operators = Mock() - module._get_ref_blockstamp_for_frame = Mock(return_value=blockstamp) - module._get_performance_threshold = Mock(return_value=1) - module.w3.csm.fee_distributor.shares_to_distribute = Mock(return_value=500) - module._calculate_distribution_in_frame = Mock(return_value=({NodeOperatorId(1): 600}, {})) - with pytest.raises(CSMError, match="Invalid distribution"): + module.w3.csm.fee_distributor.shares_to_distribute = Mock(side_effect=[500]) + module.w3.csm.get_curve_params = Mock(return_value=Mock(strikes_params=StrikesParams(lifetime=6, threshold=Mock()))) + module.w3.lido_validators = Mock(get_module_validators_by_node_operators=Mock(return_value={})) + monkeypatch.setattr(Distribution, "_get_ref_blockstamp_for_frame", Mock(return_value=blockstamp)) + monkeypatch.setattr( + Distribution, + "_calculate_distribution_in_frame", + Mock( + return_value=( + # rewards + {NodeOperatorId(1): 600}, + # distributed_rewards + 500, + # rebate_to_protocol + 0, + # strikes + {}, + ) + ), + ) + + with pytest.raises(InconsistentData, match="Invalid distribution"): module.calculate_distribution(blockstamp, last_report) -def test_calculate_distribution_in_frame_handles_no_any_duties(module: CSOracle): - frame = Mock() - threshold = 1.0 +def test_calculate_distribution_in_frame_handles_no_any_duties(module: CSOracle, monkeypatch): + frame = (1, 2) rewards_to_distribute = UINT64_MAX validator = LidoValidatorFactory.build() node_operator_id = validator.lido_id.operatorIndex operators_to_validators = {(Mock(), node_operator_id): [validator]} - module.state = State() - module.state.att_data = {frame: defaultdict(DutyAccumulator)} - module.state.prop_data = {frame: defaultdict(DutyAccumulator)} - module.state.sync_data = {frame: defaultdict(DutyAccumulator)} - module._get_performance_threshold = Mock() + + state = State() + state.data = {frame: Duties()} + state.frames = [frame] + blockstamp = Mock() + log = FramePerfLog(Mock(), frame) - rewards_distribution, strikes_in_frame = module._calculate_distribution_in_frame( - frame, threshold, rewards_to_distribute, operators_to_validators, log + distribution = Distribution(module.w3, Mock(), state) + rewards_distribution, distributed_rewards, rebate_to_protocol, strikes_in_frame = ( + distribution._calculate_distribution_in_frame( + frame, blockstamp, rewards_to_distribute, operators_to_validators, log + ) ) assert rewards_distribution[node_operator_id] == 0 @@ -170,26 +210,45 @@ def test_calculate_distribution_in_frame_handles_no_any_duties(module: CSOracle) def test_calculate_distribution_in_frame_handles_above_threshold_performance(module: CSOracle): frame = Mock() - threshold = 0.5 rewards_to_distribute = UINT64_MAX validator = LidoValidatorFactory.build() validator.validator.slashed = False node_operator_id = validator.lido_id.operatorIndex operators_to_validators = {(Mock(), node_operator_id): [validator]} - module.state = State() + state = State() attestation_duty = DutyAccumulator(assigned=10, included=6) proposal_duty = DutyAccumulator(assigned=10, included=6) sync_duty = DutyAccumulator(assigned=10, included=6) - module.state.att_data = {frame: {validator.index: attestation_duty}} - module.state.prop_data = {frame: {validator.index: proposal_duty}} - module.state.sync_data = {frame: {validator.index: sync_duty}} - module._get_performance_threshold = Mock(return_value=0.5) + state.data = { + frame: Duties( + attestations={validator.index: attestation_duty}, + proposals={validator.index: proposal_duty}, + syncs={validator.index: sync_duty}, + ) + } + state.frames = [frame] + blockstamp = Mock() + log = FramePerfLog(Mock(), frame) - rewards_distribution, strikes_in_frame = module._calculate_distribution_in_frame( - frame, threshold, rewards_to_distribute, operators_to_validators, log + module.w3.csm.get_curve_params = Mock( + return_value=Mock( + strikes_params=StrikesParams(lifetime=6, threshold=Mock()), + perf_leeway_data=Mock(get_for=Mock(return_value=0.1)), + reward_share_data=Mock(get_for=Mock(return_value=1)), + perf_coeffs=PerformanceCoefficients(), + ) + ) + + distribution = Distribution(module.w3, Mock(), state) + rewards_distribution, distributed_rewards, rebate_to_protocol, strikes_in_frame = ( + distribution._calculate_distribution_in_frame( + frame, blockstamp, rewards_to_distribute, operators_to_validators, log + ) ) + assert distributed_rewards > 0 + assert rebate_to_protocol == 0 assert rewards_distribution[node_operator_id] > 0 # no need to check exact value assert log.operators[node_operator_id].distributed > 0 assert log.operators[node_operator_id].validators[validator.index].attestation_duty == attestation_duty @@ -200,24 +259,41 @@ def test_calculate_distribution_in_frame_handles_above_threshold_performance(mod def test_calculate_distribution_in_frame_handles_below_threshold_performance(module: CSOracle): frame = Mock() - threshold = 0.5 rewards_to_distribute = UINT64_MAX validator = LidoValidatorFactory.build() validator.validator.slashed = False node_operator_id = validator.lido_id.operatorIndex operators_to_validators = {(Mock(), node_operator_id): [validator]} - module.state = State() + state = State() attestation_duty = DutyAccumulator(assigned=10, included=5) proposal_duty = DutyAccumulator(assigned=10, included=5) sync_duty = DutyAccumulator(assigned=10, included=5) - module.state.att_data = {frame: {validator.index: attestation_duty}} - module.state.prop_data = {frame: {validator.index: proposal_duty}} - module.state.sync_data = {frame: {validator.index: sync_duty}} - module._get_performance_threshold = Mock(return_value=0.5) + state.data = { + frame: Duties( + attestations={validator.index: attestation_duty}, + proposals={validator.index: proposal_duty}, + syncs={validator.index: sync_duty}, + ) + } + state.frames = [frame] + blockstamp = Mock() + log = FramePerfLog(Mock(), frame) - rewards_distribution, strikes_in_frame = module._calculate_distribution_in_frame( - frame, threshold, rewards_to_distribute, operators_to_validators, log + module.w3.csm.get_curve_params = Mock( + return_value=Mock( + strikes_params=StrikesParams(lifetime=6, threshold=Mock()), + perf_leeway_data=Mock(get_for=Mock(return_value=-0.1)), + reward_share_data=Mock(get_for=Mock(return_value=1)), + perf_coeffs=PerformanceCoefficients(), + ) + ) + + distribution = Distribution(module.w3, Mock(), state) + rewards_distribution, distributed_rewards, rebate_to_protocol, strikes_in_frame = ( + distribution._calculate_distribution_in_frame( + frame, blockstamp, rewards_to_distribute, operators_to_validators, log + ) ) assert rewards_distribution[node_operator_id] == 0 @@ -228,51 +304,6 @@ def test_calculate_distribution_in_frame_handles_below_threshold_performance(mod assert (node_operator_id, validator.pubkey) in strikes_in_frame -def test_performance_threshold_calculates_correctly(module): - state = State() - state.att_data = { - (0, 31): { - ValidatorIndex(1): DutyAccumulator(10, 10), - ValidatorIndex(2): DutyAccumulator(10, 10), - }, - } - module.w3.csm.oracle.perf_leeway_bp.return_value = 500 - module.state = state - - threshold = module._get_performance_threshold((0, 31), Mock()) - - assert threshold == 0.95 - - -def test_performance_threshold_handles_zero_leeway(module): - state = State() - state.data = { - (0, 31): { - ValidatorIndex(1): DutyAccumulator(10, 10), - ValidatorIndex(2): DutyAccumulator(10, 10), - }, - } - module.w3.csm.oracle.perf_leeway_bp.return_value = 0 - module.state = state - - threshold = module._get_performance_threshold((0, 31), Mock()) - - assert threshold == 1.0 - - -def test_performance_threshold_handles_high_leeway(module): - state = State() - state.data = { - (0, 31): {ValidatorIndex(1): DutyAccumulator(10, 1), ValidatorIndex(2): DutyAccumulator(10, 1)}, - } - module.w3.csm.oracle.perf_leeway_bp.return_value = 5000 - module.state = state - - threshold = module._get_performance_threshold((0, 31), Mock()) - - assert threshold == -0.4 - - def test_process_validator_duty_handles_above_threshold_performance(): validator = LidoValidatorFactory.build() validator.validator.slashed = False @@ -280,13 +311,30 @@ def test_process_validator_duty_handles_above_threshold_performance(): log_operator.validators = defaultdict(ValidatorFrameSummary) participation_shares = defaultdict(int) threshold = 0.5 + reward_share = 1 - attestation_duty = DutyAccumulator(assigned=10, included=6) + validator_duties = ValidatorDuties( + attestation=DutyAccumulator(assigned=10, included=6), + proposal=DutyAccumulator(assigned=10, included=6), + sync=DutyAccumulator(assigned=10, included=6), + ) - CSOracle.process_validator_duty(validator, attestation_duty, threshold, participation_shares, log_operator) + outcome = Distribution.process_validator_duties( + validator, + validator_duties, + threshold, + reward_share, + PerformanceCoefficients(), + participation_shares, + log_operator, + ) + assert outcome.strikes == 0 + assert outcome.rebate_share == 0 assert participation_shares[validator.lido_id.operatorIndex] == 10 - assert log_operator.validators[validator.index].attestation_duty == attestation_duty + assert log_operator.validators[validator.index].attestation_duty == validator_duties.attestation + assert log_operator.validators[validator.index].proposal_duty == validator_duties.proposal + assert log_operator.validators[validator.index].sync_duty == validator_duties.sync def test_process_validator_duty_handles_below_threshold_performance(): @@ -296,13 +344,30 @@ def test_process_validator_duty_handles_below_threshold_performance(): log_operator.validators = defaultdict(ValidatorFrameSummary) participation_shares = defaultdict(int) threshold = 0.5 + reward_share = 1 - attestation_duty = DutyAccumulator(assigned=10, included=4) + validator_duties = ValidatorDuties( + attestation=DutyAccumulator(assigned=10, included=4), + proposal=DutyAccumulator(assigned=10, included=4), + sync=DutyAccumulator(assigned=10, included=4), + ) - CSOracle.process_validator_duty(validator, attestation_duty, threshold, participation_shares, log_operator) + outcome = Distribution.process_validator_duties( + validator, + validator_duties, + threshold, + reward_share, + PerformanceCoefficients(), + participation_shares, + log_operator, + ) + assert outcome.strikes == 1 + assert outcome.rebate_share == 0 assert participation_shares[validator.lido_id.operatorIndex] == 0 - assert log_operator.validators[validator.index].attestation_duty == attestation_duty + assert log_operator.validators[validator.index].attestation_duty == validator_duties.attestation + assert log_operator.validators[validator.index].proposal_duty == validator_duties.proposal + assert log_operator.validators[validator.index].sync_duty == validator_duties.sync def test_process_validator_duty_handles_non_empy_participation_shares(): @@ -312,13 +377,30 @@ def test_process_validator_duty_handles_non_empy_participation_shares(): log_operator.validators = defaultdict(ValidatorFrameSummary) participation_shares = {validator.lido_id.operatorIndex: 25} threshold = 0.5 + reward_share = 1 - attestation_duty = DutyAccumulator(assigned=10, included=6) + validator_duties = ValidatorDuties( + attestation=DutyAccumulator(assigned=10, included=6), + proposal=DutyAccumulator(assigned=10, included=6), + sync=DutyAccumulator(assigned=10, included=6), + ) - CSOracle.process_validator_duty(validator, attestation_duty, threshold, participation_shares, log_operator) + outcome = Distribution.process_validator_duties( + validator, + validator_duties, + threshold, + reward_share, + PerformanceCoefficients(), + participation_shares, + log_operator, + ) + assert outcome.strikes == 0 + assert outcome.rebate_share == 0 assert participation_shares[validator.lido_id.operatorIndex] == 35 - assert log_operator.validators[validator.index].attestation_duty == attestation_duty + assert log_operator.validators[validator.index].attestation_duty == validator_duties.attestation + assert log_operator.validators[validator.index].proposal_duty == validator_duties.proposal + assert log_operator.validators[validator.index].sync_duty == validator_duties.sync def test_process_validator_duty_handles_no_duty_assigned(): @@ -327,9 +409,22 @@ def test_process_validator_duty_handles_no_duty_assigned(): log_operator.validators = defaultdict(ValidatorFrameSummary) participation_shares = defaultdict(int) threshold = 0.5 + reward_share = 1 + + validator_duties = ValidatorDuties(attestation=None, proposal=None, sync=None) + + outcome = Distribution.process_validator_duties( + validator, + validator_duties, + threshold, + reward_share, + PerformanceCoefficients(), + participation_shares, + log_operator, + ) - CSOracle.process_validator_duty(validator, None, None, None, threshold, participation_shares, log_operator) - + assert outcome.strikes == 0 + assert outcome.rebate_share == 0 assert participation_shares[validator.lido_id.operatorIndex] == 0 assert validator.index not in log_operator.validators @@ -341,15 +436,26 @@ def test_process_validator_duty_handles_slashed_validator(): log_operator.validators = defaultdict(ValidatorFrameSummary) participation_shares = defaultdict(int) threshold = 0.5 + reward_share = 1 - attestation_duty = DutyAccumulator(assigned=1, included=1) - prop_duty = DutyAccumulator(assigned=1, included=1) - sync_duty = DutyAccumulator(assigned=1, included=1) + validator_duties = ValidatorDuties( + attestation=DutyAccumulator(assigned=1, included=1), + proposal=DutyAccumulator(assigned=1, included=1), + sync=DutyAccumulator(assigned=1, included=1), + ) - CSOracle.process_validator_duty( - validator, attestation_duty, prop_duty, sync_duty, threshold, participation_shares, log_operator + outcome = Distribution.process_validator_duties( + validator, + validator_duties, + threshold, + reward_share, + PerformanceCoefficients(), + participation_shares, + log_operator, ) + assert outcome.strikes == 1 + assert outcome.rebate_share == 0 assert participation_shares[validator.lido_id.operatorIndex] == 0 assert log_operator.validators[validator.index].slashed is True @@ -357,8 +463,11 @@ def test_process_validator_duty_handles_slashed_validator(): def test_calc_rewards_distribution_in_frame_correctly_distributes_rewards(): participation_shares = {NodeOperatorId(1): 100, NodeOperatorId(2): 200} rewards_to_distribute = Wei(1 * 10**18) + rebate_share = 0 - rewards_distribution = CSOracle.calc_rewards_distribution_in_frame(participation_shares, rewards_to_distribute) + rewards_distribution = Distribution.calc_rewards_distribution_in_frame( + participation_shares, rebate_share, rewards_to_distribute + ) assert rewards_distribution[NodeOperatorId(1)] == Wei(333333333333333333) assert rewards_distribution[NodeOperatorId(2)] == Wei(666666666666666666) @@ -367,8 +476,11 @@ def test_calc_rewards_distribution_in_frame_correctly_distributes_rewards(): def test_calc_rewards_distribution_in_frame_handles_zero_participation(): participation_shares = {NodeOperatorId(1): 0, NodeOperatorId(2): 0} rewards_to_distribute = Wei(1 * 10**18) + rebate_share = 0 - rewards_distribution = CSOracle.calc_rewards_distribution_in_frame(participation_shares, rewards_to_distribute) + rewards_distribution = Distribution.calc_rewards_distribution_in_frame( + participation_shares, rebate_share, rewards_to_distribute + ) assert rewards_distribution[NodeOperatorId(1)] == 0 assert rewards_distribution[NodeOperatorId(2)] == 0 @@ -377,8 +489,11 @@ def test_calc_rewards_distribution_in_frame_handles_zero_participation(): def test_calc_rewards_distribution_in_frame_handles_no_participation(): participation_shares = {} rewards_to_distribute = Wei(1 * 10**18) + rebate_share = 0 - rewards_distribution = CSOracle.calc_rewards_distribution_in_frame(participation_shares, rewards_to_distribute) + rewards_distribution = Distribution.calc_rewards_distribution_in_frame( + participation_shares, rebate_share, rewards_to_distribute + ) assert len(rewards_distribution) == 0 @@ -386,8 +501,11 @@ def test_calc_rewards_distribution_in_frame_handles_no_participation(): def test_calc_rewards_distribution_in_frame_handles_partial_participation(): participation_shares = {NodeOperatorId(1): 100, NodeOperatorId(2): 0} rewards_to_distribute = Wei(1 * 10**18) + rebate_share = 0 - rewards_distribution = CSOracle.calc_rewards_distribution_in_frame(participation_shares, rewards_to_distribute) + rewards_distribution = Distribution.calc_rewards_distribution_in_frame( + participation_shares, rebate_share, rewards_to_distribute + ) assert rewards_distribution[NodeOperatorId(1)] == Wei(1 * 10**18) assert rewards_distribution[NodeOperatorId(2)] == 0 @@ -396,9 +514,10 @@ def test_calc_rewards_distribution_in_frame_handles_partial_participation(): def test_calc_rewards_distribution_in_frame_handles_negative_to_distribute(): participation_shares = {NodeOperatorId(1): 100, NodeOperatorId(2): 200} rewards_to_distribute = Wei(-1) + rebate_share = 0 with pytest.raises(ValueError, match="Invalid rewards to distribute"): - CSOracle.calc_rewards_distribution_in_frame(participation_shares, rewards_to_distribute) + Distribution.calc_rewards_distribution_in_frame(participation_shares, rebate_share, rewards_to_distribute) @pytest.mark.parametrize( @@ -467,6 +586,11 @@ def test_merge_strikes( threshold_per_op: dict, expected: dict, ): - module.w3.csm.get_strikes_params = Mock(side_effect=lambda no_id, _: threshold_per_op[no_id]) - module._merge_strikes(acc, strikes_in_frame, frame_blockstamp=Mock()) + distribution = Distribution(module.w3, Mock(), Mock()) + distribution.w3.csm.get_curve_params = Mock( + side_effect=lambda no_id, _: Mock(strikes_params=threshold_per_op[no_id]) + ) + + distribution._merge_strikes(acc, strikes_in_frame, frame_blockstamp=Mock()) + assert acc == expected From 0361a91842b1914a366a322716448c4a16ec6d6f Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Fri, 28 Feb 2025 14:38:26 +0100 Subject: [PATCH 093/162] refactor: validator duties outcome --- src/modules/csm/distribution.py | 29 ++++++----- tests/modules/csm/test_csm_distribution.py | 57 +++------------------- 2 files changed, 22 insertions(+), 64 deletions(-) diff --git a/src/modules/csm/distribution.py b/src/modules/csm/distribution.py index fc9e1da7b..a047ecdcd 100644 --- a/src/modules/csm/distribution.py +++ b/src/modules/csm/distribution.py @@ -26,7 +26,8 @@ class ValidatorDuties: @dataclass -class ValidatorDutyOutcome: +class ValidatorDutiesOutcome: + participation_share: int rebate_share: int strikes: int @@ -157,19 +158,19 @@ def _calculate_distribution_in_frame( duties = ValidatorDuties(att_duty, prop_duty, sync_duty) - validator_duty_outcome = self.process_validator_duties( + validator_duties_outcome = self.get_validator_duties_outcome( validator, duties, key_threshold, key_reward_share, curve_params.perf_coeffs, - participation_shares, log_operator, ) - if validator_duty_outcome.strikes: - frame_strikes[(no_id, validator.pubkey)] = validator_duty_outcome.strikes - log_operator.validators[validator.index].strikes = validator_duty_outcome.strikes - total_rebate_share += validator_duty_outcome.rebate_share + if validator_duties_outcome.strikes: + frame_strikes[(no_id, validator.pubkey)] = validator_duties_outcome.strikes + log_operator.validators[validator.index].strikes = validator_duties_outcome.strikes + participation_shares[no_id] += validator_duties_outcome.participation_share + total_rebate_share += validator_duties_outcome.rebate_share rewards_distribution = self.calc_rewards_distribution_in_frame( participation_shares, total_rebate_share, rewards_to_distribute @@ -193,19 +194,18 @@ def _get_network_performance(self, frame: Frame) -> float: return network_perf @staticmethod - def process_validator_duties( + def get_validator_duties_outcome( validator: LidoValidator, duties: ValidatorDuties, threshold: float, reward_share: float, perf_coeffs: PerformanceCoefficients, - participation_shares: defaultdict[NodeOperatorId, int], log_operator: OperatorFrameSummary, - ) -> ValidatorDutyOutcome: + ) -> ValidatorDutiesOutcome: if duties.attestation is None or duties.attestation.assigned == 0: # It's possible that the validator is not assigned to any duty, hence it's performance # is not presented in the aggregates (e.g. exited, pending for activation etc). - return ValidatorDutyOutcome(rebate_share=0, strikes=0) + return ValidatorDutiesOutcome(participation_share=0, rebate_share=0, strikes=0) log_validator = log_operator.validators[validator.index] @@ -216,7 +216,7 @@ def process_validator_duties( # It means that validator was active during the frame and got slashed and didn't meet the exit # epoch, so we should not count such validator for operator's share. log_validator.slashed = True - return ValidatorDutyOutcome(rebate_share=0, strikes=1) + return ValidatorDutiesOutcome(participation_share=0, rebate_share=0, strikes=1) performance = perf_coeffs.calc_performance(duties.attestation, duties.proposal, duties.sync) @@ -241,14 +241,13 @@ def process_validator_duties( # The rest 15 participation shares should be counted for the protocol's rebate. # participation_share = math.ceil(duties.attestation.assigned * reward_share) - participation_shares[validator.lido_id.operatorIndex] += participation_share rebate_share = duties.attestation.assigned - participation_share assert rebate_share >= 0, f"Invalid rebate share: {rebate_share=}" - return ValidatorDutyOutcome(rebate_share=rebate_share, strikes=0) + return ValidatorDutiesOutcome(participation_share, rebate_share, strikes=0) # In case of bad performance the validator should be striked and assigned attestations are not counted for # the operator's reward and rebate, so rewards will be socialized between CSM operators. - return ValidatorDutyOutcome(rebate_share=0, strikes=1) + return ValidatorDutiesOutcome(participation_share=0, rebate_share=0, strikes=1) @staticmethod def calc_rewards_distribution_in_frame( diff --git a/tests/modules/csm/test_csm_distribution.py b/tests/modules/csm/test_csm_distribution.py index 72171f282..56686f7d2 100644 --- a/tests/modules/csm/test_csm_distribution.py +++ b/tests/modules/csm/test_csm_distribution.py @@ -309,7 +309,6 @@ def test_process_validator_duty_handles_above_threshold_performance(): validator.validator.slashed = False log_operator = Mock() log_operator.validators = defaultdict(ValidatorFrameSummary) - participation_shares = defaultdict(int) threshold = 0.5 reward_share = 1 @@ -319,19 +318,18 @@ def test_process_validator_duty_handles_above_threshold_performance(): sync=DutyAccumulator(assigned=10, included=6), ) - outcome = Distribution.process_validator_duties( + outcome = Distribution.get_validator_duties_outcome( validator, validator_duties, threshold, reward_share, PerformanceCoefficients(), - participation_shares, log_operator, ) assert outcome.strikes == 0 assert outcome.rebate_share == 0 - assert participation_shares[validator.lido_id.operatorIndex] == 10 + assert outcome.participation_share == 10 assert log_operator.validators[validator.index].attestation_duty == validator_duties.attestation assert log_operator.validators[validator.index].proposal_duty == validator_duties.proposal assert log_operator.validators[validator.index].sync_duty == validator_duties.sync @@ -342,7 +340,6 @@ def test_process_validator_duty_handles_below_threshold_performance(): validator.validator.slashed = False log_operator = Mock() log_operator.validators = defaultdict(ValidatorFrameSummary) - participation_shares = defaultdict(int) threshold = 0.5 reward_share = 1 @@ -352,52 +349,18 @@ def test_process_validator_duty_handles_below_threshold_performance(): sync=DutyAccumulator(assigned=10, included=4), ) - outcome = Distribution.process_validator_duties( + outcome = Distribution.get_validator_duties_outcome( validator, validator_duties, threshold, reward_share, PerformanceCoefficients(), - participation_shares, log_operator, ) assert outcome.strikes == 1 assert outcome.rebate_share == 0 - assert participation_shares[validator.lido_id.operatorIndex] == 0 - assert log_operator.validators[validator.index].attestation_duty == validator_duties.attestation - assert log_operator.validators[validator.index].proposal_duty == validator_duties.proposal - assert log_operator.validators[validator.index].sync_duty == validator_duties.sync - - -def test_process_validator_duty_handles_non_empy_participation_shares(): - validator = LidoValidatorFactory.build() - validator.validator.slashed = False - log_operator = Mock() - log_operator.validators = defaultdict(ValidatorFrameSummary) - participation_shares = {validator.lido_id.operatorIndex: 25} - threshold = 0.5 - reward_share = 1 - - validator_duties = ValidatorDuties( - attestation=DutyAccumulator(assigned=10, included=6), - proposal=DutyAccumulator(assigned=10, included=6), - sync=DutyAccumulator(assigned=10, included=6), - ) - - outcome = Distribution.process_validator_duties( - validator, - validator_duties, - threshold, - reward_share, - PerformanceCoefficients(), - participation_shares, - log_operator, - ) - - assert outcome.strikes == 0 - assert outcome.rebate_share == 0 - assert participation_shares[validator.lido_id.operatorIndex] == 35 + assert outcome.participation_share == 0 assert log_operator.validators[validator.index].attestation_duty == validator_duties.attestation assert log_operator.validators[validator.index].proposal_duty == validator_duties.proposal assert log_operator.validators[validator.index].sync_duty == validator_duties.sync @@ -407,25 +370,23 @@ def test_process_validator_duty_handles_no_duty_assigned(): validator = LidoValidatorFactory.build() log_operator = Mock() log_operator.validators = defaultdict(ValidatorFrameSummary) - participation_shares = defaultdict(int) threshold = 0.5 reward_share = 1 validator_duties = ValidatorDuties(attestation=None, proposal=None, sync=None) - outcome = Distribution.process_validator_duties( + outcome = Distribution.get_validator_duties_outcome( validator, validator_duties, threshold, reward_share, PerformanceCoefficients(), - participation_shares, log_operator, ) assert outcome.strikes == 0 assert outcome.rebate_share == 0 - assert participation_shares[validator.lido_id.operatorIndex] == 0 + assert outcome.participation_share == 0 assert validator.index not in log_operator.validators @@ -434,7 +395,6 @@ def test_process_validator_duty_handles_slashed_validator(): validator.validator.slashed = True log_operator = Mock() log_operator.validators = defaultdict(ValidatorFrameSummary) - participation_shares = defaultdict(int) threshold = 0.5 reward_share = 1 @@ -444,19 +404,18 @@ def test_process_validator_duty_handles_slashed_validator(): sync=DutyAccumulator(assigned=1, included=1), ) - outcome = Distribution.process_validator_duties( + outcome = Distribution.get_validator_duties_outcome( validator, validator_duties, threshold, reward_share, PerformanceCoefficients(), - participation_shares, log_operator, ) assert outcome.strikes == 1 assert outcome.rebate_share == 0 - assert participation_shares[validator.lido_id.operatorIndex] == 0 + assert outcome.participation_share == 0 assert log_operator.validators[validator.index].slashed is True From 0c45b1ab2c9493bb2a0be6291d3ccf42845610b7 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Fri, 28 Feb 2025 14:38:45 +0100 Subject: [PATCH 094/162] refactor: `warning` -> `info` --- src/modules/csm/csm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index 16e2866dd..62d34ee1b 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -104,9 +104,9 @@ def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: strikes_tree_root = strikes_tree.root strikes_cid = self.publish_tree(strikes_tree) if strikes_tree_root == last_report.strikes_tree_root: - logger.warning({"msg": "Strikes tree root is the same as the previous one"}) + logger.info({"msg": "Strikes tree root is the same as the previous one"}) if strikes_cid == last_report.strikes_tree_cid: - logger.warning({"msg": "Strikes tree CID is the same as the previous one"}) + logger.info({"msg": "Strikes tree CID is the same as the previous one"}) else: strikes_tree_root = HexBytes(ZERO_HASH) strikes_cid = None From bdc8ded26b9699f029ebb8ee1299edb27ff3644f Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Thu, 6 Mar 2025 12:27:41 +0100 Subject: [PATCH 095/162] feat: distribution tests and refactoring --- src/modules/csm/distribution.py | 51 +- src/modules/csm/state.py | 32 +- .../contracts/cs_parameters_registry.py | 32 +- tests/modules/csm/test_csm_distribution.py | 1377 ++++++++++++----- tests/modules/csm/test_state.py | 119 +- 5 files changed, 1135 insertions(+), 476 deletions(-) diff --git a/src/modules/csm/distribution.py b/src/modules/csm/distribution.py index a047ecdcd..1f3e241a2 100644 --- a/src/modules/csm/distribution.py +++ b/src/modules/csm/distribution.py @@ -5,7 +5,7 @@ from src.modules.csm.helpers.last_report import LastReport from src.modules.csm.log import FramePerfLog, OperatorFrameSummary -from src.modules.csm.state import DutyAccumulator, Frame, State +from src.modules.csm.state import Frame, State, ValidatorDuties from src.modules.csm.types import Shares, StrikesList, StrikesValidator from src.providers.execution.contracts.cs_parameters_registry import PerformanceCoefficients from src.providers.execution.exceptions import InconsistentData @@ -18,13 +18,6 @@ logger = logging.getLogger(__name__) -@dataclass -class ValidatorDuties: - attestation: DutyAccumulator | None - proposal: DutyAccumulator | None - sync: DutyAccumulator | None - - @dataclass class ValidatorDutiesOutcome: participation_share: int @@ -70,7 +63,7 @@ def calculate(self, blockstamp: ReferenceBlockStamp, last_report: LastReport) -> distributed_so_far = self.total_rewards + self.total_rebate rewards_to_distribute_in_frame = total_rewards_to_distribute - distributed_so_far - frame_log = FramePerfLog(blockstamp, frame) + frame_log = FramePerfLog(frame_blockstamp, frame) ( rewards_map_in_frame, distributed_rewards_in_frame, @@ -148,15 +141,12 @@ def _calculate_distribution_in_frame( curve_params = self.w3.csm.get_curve_params(no_id, blockstamp) sorted_active_validators = sorted(active_validators, key=lambda v: v.index) - for key_number, validator in enumerate(sorted_active_validators): + numbered_validators = enumerate(sorted_active_validators, start=1) + for key_number, validator in numbered_validators: key_threshold = max(network_perf - curve_params.perf_leeway_data.get_for(key_number), 0) key_reward_share = curve_params.reward_share_data.get_for(key_number) - att_duty = self.state.data[frame].attestations.get(validator.index) - prop_duty = self.state.data[frame].proposals.get(validator.index) - sync_duty = self.state.data[frame].syncs.get(validator.index) - - duties = ValidatorDuties(att_duty, prop_duty, sync_duty) + duties = self.state.get_validator_duties(frame, validator.index) validator_duties_outcome = self.get_validator_duties_outcome( validator, @@ -172,25 +162,27 @@ def _calculate_distribution_in_frame( participation_shares[no_id] += validator_duties_outcome.participation_share total_rebate_share += validator_duties_outcome.rebate_share - rewards_distribution = self.calc_rewards_distribution_in_frame( + rewards_distribution_map = self.calc_rewards_distribution_in_frame( participation_shares, total_rebate_share, rewards_to_distribute ) - distributed_rewards = sum(rewards_distribution.values()) + distributed_rewards = sum(rewards_distribution_map.values()) + # All rewards to distribute can be rebated if no duties were assigned to validators or + # all validators were below threshold. rebate_to_protocol = rewards_to_distribute - distributed_rewards - for no_id, no_rewards in rewards_distribution.items(): + for no_id, no_rewards in rewards_distribution_map.items(): log.operators[no_id].distributed = no_rewards log.distributable = rewards_to_distribute log.distributed_rewards = distributed_rewards log.rebate_to_protocol = rebate_to_protocol - return rewards_distribution, distributed_rewards, rebate_to_protocol, frame_strikes + return rewards_distribution_map, distributed_rewards, rebate_to_protocol, frame_strikes def _get_network_performance(self, frame: Frame) -> float: - att_perf = self.state.get_att_network_aggr(frame) - prop_perf = self.state.get_prop_network_aggr(frame) - sync_perf = self.state.get_sync_network_aggr(frame) - network_perf = PerformanceCoefficients().calc_performance(att_perf, prop_perf, sync_perf) + att_aggr = self.state.get_att_network_aggr(frame) + prop_aggr = self.state.get_prop_network_aggr(frame) + sync_aggr = self.state.get_sync_network_aggr(frame) + network_perf = PerformanceCoefficients().calc_performance(ValidatorDuties(att_aggr, prop_aggr, sync_aggr)) return network_perf @staticmethod @@ -209,17 +201,16 @@ def get_validator_duties_outcome( log_validator = log_operator.validators[validator.index] - log_validator.threshold = threshold - log_validator.rewards_share = reward_share - if validator.validator.slashed is True: # It means that validator was active during the frame and got slashed and didn't meet the exit # epoch, so we should not count such validator for operator's share. log_validator.slashed = True return ValidatorDutiesOutcome(participation_share=0, rebate_share=0, strikes=1) - performance = perf_coeffs.calc_performance(duties.attestation, duties.proposal, duties.sync) + performance = perf_coeffs.calc_performance(duties) + log_validator.threshold = threshold + log_validator.rewards_share = reward_share log_validator.performance = performance log_validator.attestation_duty = duties.attestation if duties.proposal: @@ -287,6 +278,7 @@ def _merge_strikes( acc[key] = StrikesList() acc[key].push(strikes_in_frame[key]) + keys_to_delete = [] for key in acc: no_id, _ = key if key not in strikes_in_frame: @@ -295,4 +287,7 @@ def _merge_strikes( acc[key].resize(maxlen) # NOTE: Cleanup sequences like [0,0,0] since they don't bring any information. if not sum(acc[key]): - del acc[key] + keys_to_delete.append(key) + + for key in keys_to_delete: + del acc[key] diff --git a/src/modules/csm/state.py b/src/modules/csm/state.py index 1acfbec03..4bebb658f 100644 --- a/src/modules/csm/state.py +++ b/src/modules/csm/state.py @@ -40,7 +40,14 @@ def merge(self, other: Self) -> None: @dataclass -class Duties: +class ValidatorDuties: + attestation: DutyAccumulator | None + proposal: DutyAccumulator | None + sync: DutyAccumulator | None + + +@dataclass +class NetworkDuties: attestations: defaultdict[ValidatorIndex, DutyAccumulator] = field(default_factory=lambda: defaultdict(DutyAccumulator)) proposals: defaultdict[ValidatorIndex, DutyAccumulator] = field(default_factory=lambda: defaultdict(DutyAccumulator)) syncs: defaultdict[ValidatorIndex, DutyAccumulator] = field(default_factory=lambda: defaultdict(DutyAccumulator)) @@ -55,7 +62,7 @@ def merge(self, other: Self) -> None: type Frame = tuple[EpochNumber, EpochNumber] -type StateData = dict[Frame, Duties] +type StateData = dict[Frame, NetworkDuties] class State: @@ -201,7 +208,7 @@ def _migrate_frames_data(self, new_frames: list[Frame]): logger.info({"msg": f"Migrating duties data cache: {self.frames=} -> {new_frames=}"}) new_data: StateData = {} for frame in new_frames: - new_data[frame] = Duties() + new_data[frame] = NetworkDuties() def overlaps(a: Frame, b: Frame): return a[0] <= b[0] and a[1] >= b[1] @@ -232,12 +239,23 @@ def validate(self, l_epoch: EpochNumber, r_epoch: EpochNumber) -> None: if epoch not in self._processed_epochs: raise InvalidState(f"Epoch {epoch} missing in processed epochs") + def get_validator_duties(self, frame: Frame, validator_index: ValidatorIndex) -> ValidatorDuties: + frame_data = self.data.get(frame) + if frame_data is None: + raise ValueError(f"No data for frame: {frame=}") + + att_duty = frame_data.attestations.get(validator_index) + prop_duty = frame_data.proposals.get(validator_index) + sync_duty = frame_data.syncs.get(validator_index) + + return ValidatorDuties(att_duty, prop_duty, sync_duty) + def get_att_network_aggr(self, frame: Frame) -> DutyAccumulator: # TODO: exclude `active_slashed` validators from the calculation frame_data = self.data.get(frame) if frame_data is None: raise ValueError(f"No data for frame: {frame=}") - aggr = self.get_duty_network_aggr(frame_data.attestations) + aggr = self._get_duty_network_aggr(frame_data.attestations) logger.info({"msg": "Network attestations aggregate computed", "value": repr(aggr), "avg_perf": aggr.perf}) return aggr @@ -245,7 +263,7 @@ def get_prop_network_aggr(self, frame: Frame) -> DutyAccumulator: frame_data = self.data.get(frame) if frame_data is None: raise ValueError(f"No data for frame: {frame=}") - aggr = self.get_duty_network_aggr(frame_data.proposals) + aggr = self._get_duty_network_aggr(frame_data.proposals) logger.info({"msg": "Network proposal aggregate computed", "value": repr(aggr), "avg_perf": aggr.perf}) return aggr @@ -253,12 +271,12 @@ def get_sync_network_aggr(self, frame: Frame) -> DutyAccumulator: frame_data = self.data.get(frame) if frame_data is None: raise ValueError(f"No data for frame: {frame=}") - aggr = self.get_duty_network_aggr(frame_data.syncs) + aggr = self._get_duty_network_aggr(frame_data.syncs) logger.info({"msg": "Network syncs aggregate computed", "value": repr(aggr), "avg_perf": aggr.perf}) return aggr @staticmethod - def get_duty_network_aggr(duty_frame_data: defaultdict[ValidatorIndex, DutyAccumulator]) -> DutyAccumulator: + def _get_duty_network_aggr(duty_frame_data: defaultdict[ValidatorIndex, DutyAccumulator]) -> DutyAccumulator: included = assigned = 0 for validator, acc in duty_frame_data.items(): if acc.included > acc.assigned: diff --git a/src/providers/execution/contracts/cs_parameters_registry.py b/src/providers/execution/contracts/cs_parameters_registry.py index 190eedcf0..4a65f4e9e 100644 --- a/src/providers/execution/contracts/cs_parameters_registry.py +++ b/src/providers/execution/contracts/cs_parameters_registry.py @@ -4,6 +4,7 @@ from web3.types import BlockIdentifier from src.constants import TOTAL_BASIS_POINTS, UINT256_MAX +from src.modules.csm.state import ValidatorDuties from src.providers.execution.base_interface import ContractInterface from src.utils.cache import global_lru_cache as lru_cache @@ -16,17 +17,21 @@ class PerformanceCoefficients: blocks_weight: int = 8 sync_weight: int = 2 - def calc_performance(self, att_aggr, prop_aggr, sync_aggr) -> float: - base = self.attestations_weight - performance = att_aggr.perf * self.attestations_weight + def calc_performance(self, duties: ValidatorDuties) -> float: + base = 0 + performance = 0.0 - if prop_aggr: + if duties.attestation: + base += self.attestations_weight + performance += duties.attestation.perf * self.attestations_weight + + if duties.proposal: base += self.blocks_weight - performance += prop_aggr.perf * self.blocks_weight + performance += duties.proposal.perf * self.blocks_weight - if sync_aggr: + if duties.sync: base += self.sync_weight - performance += sync_aggr.perf * self.sync_weight + performance += duties.sync.perf * self.sync_weight performance /= base @@ -42,10 +47,14 @@ class RewardShare: reward_shares: list[int] def get_for(self, key_number: int) -> float: + if key_number <= 0: + raise ValueError("Key number should be greater than 0") for i, pivot_number in enumerate(self.key_pivots + [UINT256_MAX]): if key_number <= pivot_number: + if i >= len(self.reward_shares): + continue return self.reward_shares[i] / TOTAL_BASIS_POINTS - raise ValueError(f"Key number {key_number} is out of {self.key_pivots}") + raise ValueError(f"Can't find performance leeway for {key_number=}. {self=}") @dataclass @@ -54,11 +63,14 @@ class PerformanceLeeway: performance_leeways: list[int] def get_for(self, key_number: int) -> float: + if key_number <= 0: + raise ValueError("Key number should be greater than 0") for i, pivot_number in enumerate(self.key_pivots + [UINT256_MAX]): if key_number <= pivot_number: + if i >= len(self.performance_leeways): + continue return self.performance_leeways[i] / TOTAL_BASIS_POINTS - raise ValueError(f"Key number {key_number} is out of {self.key_pivots}") - + raise ValueError(f"Can't find performance leeway for {key_number=}. {self=}") @dataclass class StrikesParams: diff --git a/tests/modules/csm/test_csm_distribution.py b/tests/modules/csm/test_csm_distribution.py index 56686f7d2..dc4483585 100644 --- a/tests/modules/csm/test_csm_distribution.py +++ b/tests/modules/csm/test_csm_distribution.py @@ -1,408 +1,906 @@ from collections import defaultdict -from unittest.mock import Mock, call, patch +from unittest.mock import Mock import pytest +from hexbytes import HexBytes from web3.types import Wei -from src.constants import UINT64_MAX +from src.constants import TOTAL_BASIS_POINTS from src.modules.csm.csm import CSOracle -from src.modules.csm.distribution import Distribution, ValidatorDuties -from src.modules.csm.log import FramePerfLog, ValidatorFrameSummary -from src.modules.csm.state import DutyAccumulator, State, Duties +from src.modules.csm.distribution import Distribution, ValidatorDuties, ValidatorDutiesOutcome +from src.modules.csm.log import FramePerfLog, ValidatorFrameSummary, OperatorFrameSummary +from src.modules.csm.state import DutyAccumulator, State, NetworkDuties, Frame from src.modules.csm.types import StrikesList -from src.providers.execution.contracts.cs_parameters_registry import StrikesParams, PerformanceCoefficients +from src.providers.execution.contracts.cs_fee_distributor import CSFeeDistributorContract +from src.providers.execution.contracts.cs_parameters_registry import ( + StrikesParams, + PerformanceCoefficients, + CurveParams, + PerformanceLeeway, + RewardShare, +) from src.providers.execution.exceptions import InconsistentData -from src.types import NodeOperatorId +from src.types import NodeOperatorId, EpochNumber, ValidatorIndex from src.web3py.extensions import CSM +from src.web3py.types import Web3 from tests.factory.blockstamp import ReferenceBlockStampFactory -from tests.factory.no_registry import LidoValidatorFactory - - -@pytest.fixture() -def module(web3, csm: CSM): - yield CSOracle(web3) - - -def test_calculate_distribution_handles_single_frame(module: CSOracle, monkeypatch): - module.converter = Mock() - module.state = Mock() - module.state.frames = [(1, 2)] - blockstamp = ReferenceBlockStampFactory.build(ref_epoch=2) - last_report = Mock(strikes={}, rewards=[]) +from tests.factory.no_registry import LidoValidatorFactory, ValidatorStateFactory - module.w3.csm.fee_distributor.shares_to_distribute = Mock(side_effect=[500]) - module.w3.csm.get_curve_params = Mock(return_value=Mock(strikes_params=StrikesParams(lifetime=6, threshold=Mock()))) - module.w3.lido_validators = Mock(get_module_validators_by_node_operators=Mock(return_value={})) - monkeypatch.setattr( - Distribution, - "_calculate_distribution_in_frame", - Mock( - return_value=( - # rewards - { - NodeOperatorId(1): 500, +@pytest.mark.parametrize( + "frames, " + "last_report, " + "mocked_curve_params, " + "frame_blockstamps, " + "shares_to_distribute, " + "distribution_in_frame, " + "expected_total_rewards, " + "expected_total_rewards_map, " + "expected_total_rebate," + "expected_strikes", + [ + # One frame + ( + [(0, 31)], + Mock( + strikes={(NodeOperatorId(1), HexBytes("0x01")): StrikesList([1, 0, 0, 0, 1, 1])}, + rewards=[(NodeOperatorId(1), 500)], + ), + Mock( + return_value=CurveParams( + strikes_params=StrikesParams(lifetime=6, threshold=...), + perf_leeway_data=..., + reward_share_data=..., + perf_coeffs=..., + ) + ), + [ReferenceBlockStampFactory.build(ref_epoch=31)], + [500], + [ + ( + # rewards + {NodeOperatorId(1): 500}, + # distributed_rewards + 500, + # rebate_to_protocol + 0, + # strikes + {(NodeOperatorId(1), HexBytes("0x01")): 1}, + ) + ], + 500, + {NodeOperatorId(1): 1000}, + 0, + {(NodeOperatorId(1), HexBytes("0x01")): [1, 1, 0, 0, 0, 1]}, + ), + # One frame, no strikes and rewards before + ( + [(0, 31)], + Mock(strikes={}, rewards=[]), + Mock( + return_value=CurveParams( + strikes_params=StrikesParams(lifetime=6, threshold=...), + perf_leeway_data=..., + reward_share_data=..., + perf_coeffs=..., + ) + ), + [ReferenceBlockStampFactory.build(ref_epoch=31)], + [500], + [ + ( + # rewards + {NodeOperatorId(1): 500}, + # distributed_rewards + 500, + # rebate_to_protocol + 0, + # strikes + {(NodeOperatorId(1), HexBytes("0x01")): 1}, + ) + ], + 500, + {NodeOperatorId(1): 500}, + 0, + {(NodeOperatorId(1), HexBytes("0x01")): [1, 0, 0, 0, 0, 0]}, + ), + # Multiple frames + ( + [(0, 31), (32, 63), (64, 95)], + Mock( + strikes={ + (NodeOperatorId(1), HexBytes("0x01")): StrikesList([1, 0, 0, 0, 1, 1]), + (NodeOperatorId(100500), HexBytes("0x100500")): StrikesList([0, 0, 0, 1, 1, 1]), }, - # distributed_rewards + rewards=[(NodeOperatorId(1), 500)], + ), + Mock( + return_value=CurveParams( + strikes_params=StrikesParams(lifetime=6, threshold=...), + perf_leeway_data=..., + reward_share_data=..., + perf_coeffs=..., + ) + ), + [ + ReferenceBlockStampFactory.build(ref_epoch=31), + ReferenceBlockStampFactory.build(ref_epoch=63), + ReferenceBlockStampFactory.build(ref_epoch=95), + ], + [ 500, - # rebate_to_protocol - 0, - # strikes - { - (NodeOperatorId(0), b"42"): 1, - (NodeOperatorId(2), b"17"): 2, - }, - ) - ), - ) - - distribution = module.calculate_distribution(blockstamp, last_report) - - assert distribution.total_rewards == 500 - assert distribution.total_rewards_map[NodeOperatorId(1)] == 500 - assert distribution.strikes == { - (NodeOperatorId(0), b"42"): [1, 0, 0, 0, 0, 0], - (NodeOperatorId(2), b"17"): [2, 0, 0, 0, 0, 0], - } - assert len(distribution.logs) == 1 - - -def test_calculate_distribution_handles_multiple_frames(module: CSOracle, monkeypatch): - module.converter = Mock() - module.state = Mock() - module.state.frames = [(1, 2), (3, 4), (5, 6)] - blockstamp = ReferenceBlockStampFactory.build(ref_epoch=2) - last_report = Mock(strikes={}, rewards=[]) - - module.w3.csm.fee_distributor.shares_to_distribute = Mock(side_effect=[500, 1500, 1600]) - module.w3.csm.get_curve_params = Mock(return_value=Mock(strikes_params=StrikesParams(lifetime=6, threshold=Mock()))) - module.w3.lido_validators = Mock(get_module_validators_by_node_operators=Mock(return_value={})) - - monkeypatch.setattr(Distribution, "_get_ref_blockstamp_for_frame", Mock(return_value=blockstamp)) - monkeypatch.setattr( - Distribution, - "_calculate_distribution_in_frame", - Mock( - side_effect=[ + 500 + 700, + 500 + 700 + 300, + ], + [ ( # rewards - { - NodeOperatorId(1): 500, - }, + {NodeOperatorId(1): 500}, # distributed_rewards 500, # rebate_to_protocol 0, # strikes - { - (NodeOperatorId(0), b"42"): 1, - (NodeOperatorId(2), b"17"): 2, - }, + {(NodeOperatorId(1), HexBytes("0x01")): 1}, ), ( # rewards - { - NodeOperatorId(1): 136, - NodeOperatorId(3): 777, - }, + {NodeOperatorId(1): 700}, # distributed_rewards - 913, + 700, # rebate_to_protocol 0, # strikes - { - (NodeOperatorId(0), b"42"): 3, - }, + {(NodeOperatorId(2), HexBytes("0x02")): 1}, ), ( # rewards - { - NodeOperatorId(1): 164, - }, + {NodeOperatorId(1): 300}, # distributed_rewards - 164, + 300, # rebate_to_protocol 0, # strikes - { - (NodeOperatorId(2), b"17"): 4, - (NodeOperatorId(2), b"18"): 1, - }, + {}, ), - ] + ], + 1500, + {NodeOperatorId(1): 2000}, + 0, + { + (NodeOperatorId(1), HexBytes("0x01")): [0, 0, 1, 1, 0, 0], + (NodeOperatorId(2), HexBytes("0x02")): [0, 1, 0, 0, 0, 0], + }, ), - ) - - distribution = module.calculate_distribution(blockstamp, last_report) - - assert distribution.total_rewards == 800 + 777 - assert distribution.total_rewards_map[NodeOperatorId(1)] == 800 - assert distribution.total_rewards_map[NodeOperatorId(3)] == 777 - assert distribution.strikes == { - (NodeOperatorId(0), b"42"): [0, 3, 1, 0, 0, 0], - (NodeOperatorId(2), b"17"): [4, 0, 2, 0, 0, 0], - (NodeOperatorId(2), b"18"): [1, 0, 0, 0, 0, 0], - } - assert len(distribution.logs) == len(module.state.frames) - Distribution._get_ref_blockstamp_for_frame.assert_has_calls( - [call(blockstamp, frame[1]) for frame in module.state.frames[1:]] - ) - - -def test_calculate_distribution_handles_invalid_distribution(module: CSOracle, monkeypatch): - module.converter = Mock() - module.state = Mock() - module.state.frames = [(1, 2)] - blockstamp = Mock() - last_report = Mock(strikes={}, rewards=[]) - - module.w3.csm.fee_distributor.shares_to_distribute = Mock(side_effect=[500]) - module.w3.csm.get_curve_params = Mock(return_value=Mock(strikes_params=StrikesParams(lifetime=6, threshold=Mock()))) - module.w3.lido_validators = Mock(get_module_validators_by_node_operators=Mock(return_value={})) - monkeypatch.setattr(Distribution, "_get_ref_blockstamp_for_frame", Mock(return_value=blockstamp)) - monkeypatch.setattr( - Distribution, - "_calculate_distribution_in_frame", - Mock( - return_value=( - # rewards - {NodeOperatorId(1): 600}, - # distributed_rewards + # One frame with no distribution + ( + [(0, 31)], + Mock( + strikes={(NodeOperatorId(1), HexBytes("0x01")): StrikesList([1, 0, 0, 0, 1, 1])}, + rewards=[(NodeOperatorId(1), 500)], + ), + Mock( + return_value=CurveParams( + strikes_params=StrikesParams(lifetime=6, threshold=...), + perf_leeway_data=..., + reward_share_data=..., + perf_coeffs=..., + ) + ), + [ReferenceBlockStampFactory.build(ref_epoch=31)], + [500], + [ + ( + # rewards + {}, + # distributed_rewards + 0, + # rebate_to_protocol + 500, + # strikes + {(NodeOperatorId(1), HexBytes("0x01")): 1}, + ) + ], + 0, + {NodeOperatorId(1): 500}, + 500, + {(NodeOperatorId(1), HexBytes("0x01")): [1, 1, 0, 0, 0, 1]}, + ), + # Multiple frames, some of which are not distributed + ( + [(0, 31), (32, 63), (64, 95)], + Mock( + strikes={ + (NodeOperatorId(1), HexBytes("0x01")): StrikesList([1, 0, 0, 0, 1, 1]), + (NodeOperatorId(100500), HexBytes("0x100500")): StrikesList([0, 0, 0, 1, 1, 1]), + }, + rewards=[(NodeOperatorId(1), 500)], + ), + Mock( + return_value=CurveParams( + strikes_params=StrikesParams(lifetime=6, threshold=...), + perf_leeway_data=..., + reward_share_data=..., + perf_coeffs=..., + ) + ), + [ + ReferenceBlockStampFactory.build(ref_epoch=31), + ReferenceBlockStampFactory.build(ref_epoch=63), + ReferenceBlockStampFactory.build(ref_epoch=95), + ], + [ 500, - # rebate_to_protocol - 0, - # strikes - {}, - ) + 500 + 700, + 500 + 700 + 300, + ], + [ + ( + # rewards + {}, + # distributed_rewards + 0, + # rebate_to_protocol + 500, + # strikes + {(NodeOperatorId(1), HexBytes("0x01")): 1}, + ), + ( + # rewards + {NodeOperatorId(1): 700}, + # distributed_rewards + 700, + # rebate_to_protocol + 0, + # strikes + {(NodeOperatorId(2), HexBytes("0x02")): 1}, + ), + ( + # rewards + {}, + # distributed_rewards + 0, + # rebate_to_protocol + 300, + # strikes + {}, + ), + ], + 700, + {NodeOperatorId(1): 500 + 700}, + 800, + { + (NodeOperatorId(1), HexBytes("0x01")): [0, 0, 1, 1, 0, 0], + (NodeOperatorId(2), HexBytes("0x02")): [0, 1, 0, 0, 0, 0], + }, ), - ) - - with pytest.raises(InconsistentData, match="Invalid distribution"): - module.calculate_distribution(blockstamp, last_report) - - -def test_calculate_distribution_in_frame_handles_no_any_duties(module: CSOracle, monkeypatch): - frame = (1, 2) - rewards_to_distribute = UINT64_MAX - validator = LidoValidatorFactory.build() - node_operator_id = validator.lido_id.operatorIndex - operators_to_validators = {(Mock(), node_operator_id): [validator]} - - state = State() - state.data = {frame: Duties()} - state.frames = [frame] - blockstamp = Mock() - - log = FramePerfLog(Mock(), frame) - - distribution = Distribution(module.w3, Mock(), state) - rewards_distribution, distributed_rewards, rebate_to_protocol, strikes_in_frame = ( - distribution._calculate_distribution_in_frame( - frame, blockstamp, rewards_to_distribute, operators_to_validators, log + ], +) +def test_calculate_distribution( + frames: list[Frame], + last_report, + mocked_curve_params, + frame_blockstamps, + shares_to_distribute, + distribution_in_frame, + expected_total_rewards, + expected_total_rewards_map, + expected_total_rebate, + expected_strikes, +): + # Mocking the data from EL + w3 = Mock(spec=Web3, csm=Mock(spec=CSM, fee_distributor=Mock(spec=CSFeeDistributorContract))) + w3.csm.fee_distributor.shares_to_distribute = Mock(side_effect=shares_to_distribute) + w3.csm.get_curve_params = mocked_curve_params + + distribution = Distribution(w3, converter=..., state=State()) + distribution._get_module_validators = Mock(...) + distribution.state.frames = frames + distribution._get_frame_blockstamp = Mock(side_effect=frame_blockstamps) + distribution._calculate_distribution_in_frame = Mock(side_effect=distribution_in_frame) + + distribution.calculate(blockstamp=..., last_report=last_report) + + assert distribution.total_rewards == expected_total_rewards + assert distribution.total_rewards_map == expected_total_rewards_map + assert distribution.total_rebate == expected_total_rebate + assert distribution.strikes == expected_strikes + + assert len(distribution.logs) == len(frames) + for i, log in enumerate(distribution.logs): + assert log.blockstamp == frame_blockstamps[i] + assert log.frame == frames[i] + + +def test_calculate_distribution_handles_invalid_distribution(): + # Mocking the data from EL + w3 = Mock(spec=Web3, csm=Mock(spec=CSM, fee_distributor=Mock(spec=CSFeeDistributorContract))) + w3.csm.fee_distributor.shares_to_distribute = Mock(return_value=500) + w3.csm.get_curve_params = Mock(...) + + distribution = Distribution(w3, converter=..., state=State()) + distribution._get_module_validators = Mock(...) + distribution.state.frames = [(EpochNumber(0), EpochNumber(31))] + distribution._get_frame_blockstamp = Mock(return_value=ReferenceBlockStampFactory.build(ref_epoch=31)) + distribution._calculate_distribution_in_frame = Mock( + return_value=( + # rewards + {NodeOperatorId(1): 500}, + # distributed_rewards + 500, + # rebate_to_protocol + 1, + # strikes + {}, ) ) - assert rewards_distribution[node_operator_id] == 0 - assert log.operators[node_operator_id].distributed == 0 - assert log.operators[node_operator_id].validators == defaultdict(ValidatorFrameSummary) - assert not strikes_in_frame - - -def test_calculate_distribution_in_frame_handles_above_threshold_performance(module: CSOracle): - frame = Mock() - rewards_to_distribute = UINT64_MAX - validator = LidoValidatorFactory.build() - validator.validator.slashed = False - node_operator_id = validator.lido_id.operatorIndex - operators_to_validators = {(Mock(), node_operator_id): [validator]} - state = State() - attestation_duty = DutyAccumulator(assigned=10, included=6) - proposal_duty = DutyAccumulator(assigned=10, included=6) - sync_duty = DutyAccumulator(assigned=10, included=6) - state.data = { - frame: Duties( - attestations={validator.index: attestation_duty}, - proposals={validator.index: proposal_duty}, - syncs={validator.index: sync_duty}, - ) - } - state.frames = [frame] - blockstamp = Mock() - - log = FramePerfLog(Mock(), frame) - - module.w3.csm.get_curve_params = Mock( - return_value=Mock( - strikes_params=StrikesParams(lifetime=6, threshold=Mock()), - perf_leeway_data=Mock(get_for=Mock(return_value=0.1)), - reward_share_data=Mock(get_for=Mock(return_value=1)), - perf_coeffs=PerformanceCoefficients(), + with pytest.raises(ValueError, match="Invalid distribution: 501 > 500"): + distribution.calculate(..., Mock(strikes={}, rewards=[])) + + +def test_calculate_distribution_handles_invalid_distribution_in_total(): + # Mocking the data from EL + w3 = Mock(spec=Web3, csm=Mock(spec=CSM, fee_distributor=Mock(spec=CSFeeDistributorContract))) + w3.csm.fee_distributor.shares_to_distribute = Mock(return_value=500) + w3.csm.get_curve_params = Mock(...) + + distribution = Distribution(w3, converter=..., state=State()) + distribution._get_module_validators = Mock(...) + distribution.state.frames = [(EpochNumber(0), EpochNumber(31))] + distribution._get_frame_blockstamp = Mock(return_value=ReferenceBlockStampFactory.build(ref_epoch=31)) + distribution._calculate_distribution_in_frame = Mock( + return_value=( + # rewards + {NodeOperatorId(1): 500}, + # distributed_rewards + 400, + # rebate_to_protocol + 1, + # strikes + {}, ) ) - distribution = Distribution(module.w3, Mock(), state) - rewards_distribution, distributed_rewards, rebate_to_protocol, strikes_in_frame = ( - distribution._calculate_distribution_in_frame( - frame, blockstamp, rewards_to_distribute, operators_to_validators, log - ) - ) + with pytest.raises(InconsistentData, match="Invalid distribution"): + distribution.calculate(..., Mock(strikes={}, rewards=[])) - assert distributed_rewards > 0 - assert rebate_to_protocol == 0 - assert rewards_distribution[node_operator_id] > 0 # no need to check exact value - assert log.operators[node_operator_id].distributed > 0 - assert log.operators[node_operator_id].validators[validator.index].attestation_duty == attestation_duty - assert log.operators[node_operator_id].validators[validator.index].proposal_duty == proposal_duty - assert log.operators[node_operator_id].validators[validator.index].sync_duty == sync_duty - assert not strikes_in_frame +@pytest.mark.parametrize( + "to_distribute, " + "frame_validators, " + "frame_state_data, " + "mocked_curve_params, " + "expected_rewards_distribution_map, " + "expected_distributed_rewards, " + "expected_rebate_to_protocol, " + "expected_frame_strikes, " + "expected_log", + [ + # All above threshold performance + ( + 100, + { + (..., NodeOperatorId(1)): [ + LidoValidatorFactory.build( + index=ValidatorIndex(1), validator=ValidatorStateFactory.build(slashed=False) + ), + ], + }, + NetworkDuties( + attestations=defaultdict( + DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(assigned=10, included=6)} + ), + proposals=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(assigned=10, included=6)}), + syncs=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(assigned=10, included=6)}), + ), + Mock( + return_value=CurveParams( + strikes_params=..., + perf_leeway_data=Mock(get_for=Mock(return_value=0.1)), + reward_share_data=Mock(get_for=Mock(return_value=1)), + perf_coeffs=PerformanceCoefficients(), + ) + ), + # Expected: + # Rewards map + { + NodeOperatorId(1): 100, + }, + # Distributed rewards + 100, + # Rebate to protocol + 0, + # Strikes + {}, + FramePerfLog( + blockstamp=..., + frame=..., + distributable=100, + distributed_rewards=100, + rebate_to_protocol=0, + operators={ + NodeOperatorId(1): OperatorFrameSummary( + distributed=100, + performance_coefficients=PerformanceCoefficients(), + validators={ + ValidatorIndex(1): ValidatorFrameSummary( + performance=0.6, + threshold=0.5, + rewards_share=1, + slashed=False, + strikes=0, + attestation_duty=DutyAccumulator(assigned=10, included=6), + proposal_duty=DutyAccumulator(assigned=10, included=6), + sync_duty=DutyAccumulator(assigned=10, included=6), + ) + }, + ) + }, + ), + ), + # All below threshold performance + ( + 100, + { + (..., NodeOperatorId(1)): [ + LidoValidatorFactory.build( + index=ValidatorIndex(1), validator=ValidatorStateFactory.build(slashed=False, pubkey="0x01") + ), + ], + }, + NetworkDuties( + attestations=defaultdict( + DutyAccumulator, + { + ValidatorIndex(1): DutyAccumulator(assigned=10, included=5), + ValidatorIndex(2): DutyAccumulator(assigned=10, included=10), + }, + ), + proposals=defaultdict( + DutyAccumulator, + { + ValidatorIndex(1): DutyAccumulator(assigned=10, included=5), + ValidatorIndex(2): DutyAccumulator(assigned=10, included=10), + }, + ), + syncs=defaultdict( + DutyAccumulator, + { + ValidatorIndex(1): DutyAccumulator(assigned=10, included=5), + ValidatorIndex(2): DutyAccumulator(assigned=10, included=10), + }, + ), + ), + Mock( + return_value=CurveParams( + strikes_params=..., + perf_leeway_data=Mock(get_for=Mock(return_value=0.1)), + reward_share_data=Mock(get_for=Mock(return_value=1)), + perf_coeffs=PerformanceCoefficients(), + ) + ), + # Expected: + # Distribution map + {}, + # Distributed rewards + 0, + # Rebate to protocol + 100, + # Strikes + { + (NodeOperatorId(1), HexBytes('0x01')): 1, + }, + FramePerfLog( + blockstamp=..., + frame=..., + distributable=100, + distributed_rewards=0, + rebate_to_protocol=100, + operators={ + NodeOperatorId(1): OperatorFrameSummary( + distributed=0, + performance_coefficients=PerformanceCoefficients(), + validators={ + ValidatorIndex(1): ValidatorFrameSummary( + performance=0.5, + threshold=0.65, + rewards_share=1, + slashed=False, + strikes=1, + attestation_duty=DutyAccumulator(assigned=10, included=5), + proposal_duty=DutyAccumulator(assigned=10, included=5), + sync_duty=DutyAccumulator(assigned=10, included=5), + ) + }, + ) + }, + ), + ), + # Mixed. With custom threshold and reward share + ( + 100, + { + # Operator 1. One above threshold performance, one slashed + (..., NodeOperatorId(1)): [ + LidoValidatorFactory.build( + index=ValidatorIndex(1), validator=ValidatorStateFactory.build(slashed=False, pubkey="0x01") + ), + LidoValidatorFactory.build( + index=ValidatorIndex(2), validator=ValidatorStateFactory.build(slashed=True, pubkey="0x02") + ), + ], + # Operator 2. One above threshold performance, one below + (..., NodeOperatorId(2)): [ + LidoValidatorFactory.build( + index=ValidatorIndex(3), validator=ValidatorStateFactory.build(slashed=False, pubkey="0x03") + ), + LidoValidatorFactory.build( + index=ValidatorIndex(4), validator=ValidatorStateFactory.build(slashed=False, pubkey="0x04") + ), + ], + # Operator 3. All below threshold performance + (..., NodeOperatorId(3)): [ + LidoValidatorFactory.build( + index=ValidatorIndex(5), validator=ValidatorStateFactory.build(slashed=False, pubkey="0x05") + ), + ], + # Operator 4. No duties + (..., NodeOperatorId(4)): [ + LidoValidatorFactory.build( + index=ValidatorIndex(6), validator=ValidatorStateFactory.build(slashed=False, pubkey="0x06") + ), + ], + # Operator 5. All above threshold performance + (..., NodeOperatorId(5)): [ + LidoValidatorFactory.build( + index=ValidatorIndex(7), validator=ValidatorStateFactory.build(slashed=False, pubkey="0x07") + ), + LidoValidatorFactory.build( + index=ValidatorIndex(8), validator=ValidatorStateFactory.build(slashed=False, pubkey="0x08") + ), + ], + }, + NetworkDuties( + attestations=defaultdict( + DutyAccumulator, + { + ValidatorIndex(1): DutyAccumulator(assigned=10, included=10), + ValidatorIndex(2): DutyAccumulator(assigned=10, included=10), + ValidatorIndex(3): DutyAccumulator(assigned=10, included=10), + ValidatorIndex(4): DutyAccumulator(assigned=10, included=0), + ValidatorIndex(5): DutyAccumulator(assigned=10, included=0), + ValidatorIndex(7): DutyAccumulator(assigned=10, included=10), + ValidatorIndex(8): DutyAccumulator(assigned=10, included=10), + # Network validator + ValidatorIndex(100500): DutyAccumulator(assigned=1000, included=1000), + }, + ), + proposals=defaultdict( + DutyAccumulator, + { + ValidatorIndex(1): DutyAccumulator(assigned=10, included=10), + ValidatorIndex(4): DutyAccumulator(assigned=10, included=10), + ValidatorIndex(7): DutyAccumulator(assigned=10, included=10), + # Network validator + ValidatorIndex(100500): DutyAccumulator(assigned=1000, included=1000), + }, + ), + syncs=defaultdict( + DutyAccumulator, + { + ValidatorIndex(2): DutyAccumulator(assigned=10, included=10), + ValidatorIndex(3): DutyAccumulator(assigned=10, included=10), + ValidatorIndex(8): DutyAccumulator(assigned=10, included=10), + # Network validator + ValidatorIndex(100500): DutyAccumulator(assigned=1000, included=1000), + }, + ), + ), + Mock( + side_effect=lambda no_id, _: { + NodeOperatorId(5): CurveParams( + strikes_params=..., + perf_leeway_data=PerformanceLeeway(key_pivots=[1], performance_leeways=[1000, 2000]), + reward_share_data=RewardShare(key_pivots=[1], reward_shares=[10000, 9000]), + perf_coeffs=PerformanceCoefficients(attestations_weight=1, blocks_weight=0, sync_weight=0), + ), + }.get( + no_id, + CurveParams( + strikes_params=..., + perf_leeway_data=Mock(get_for=Mock(return_value=0.1)), + reward_share_data=Mock(get_for=Mock(return_value=1)), + perf_coeffs=PerformanceCoefficients(), + ), + ), + ), + # Expected: + # Distribution map + { + NodeOperatorId(1): 25, + NodeOperatorId(2): 25, + NodeOperatorId(5): 47, + }, + # Distributed rewards + 97, + # Rebate to protocol + 3, + # Strikes + { + (NodeOperatorId(1), HexBytes('0x02')): 1, # Slashed + (NodeOperatorId(2), HexBytes('0x04')): 1, # Below threshold + (NodeOperatorId(3), HexBytes('0x05')): 1, # Below threshold + }, + FramePerfLog( + blockstamp=..., + frame=..., + distributable=100, + distributed_rewards=97, + rebate_to_protocol=3, + operators=defaultdict( + OperatorFrameSummary, + { + NodeOperatorId(1): OperatorFrameSummary( + distributed=25, + performance_coefficients=PerformanceCoefficients(), + validators=defaultdict( + ValidatorFrameSummary, + { + ValidatorIndex(1): ValidatorFrameSummary( + performance=1.0, + threshold=0.8842289719626168, + rewards_share=1, + attestation_duty=DutyAccumulator(assigned=10, included=10), + proposal_duty=DutyAccumulator(assigned=10, included=10), + ), + ValidatorIndex(2): ValidatorFrameSummary( + slashed=True, + strikes=1, + ), + }, + ), + ), + NodeOperatorId(2): OperatorFrameSummary( + distributed=25, + performance_coefficients=PerformanceCoefficients(), + validators=defaultdict( + ValidatorFrameSummary, + { + ValidatorIndex(3): ValidatorFrameSummary( + performance=1.0, + threshold=0.8842289719626168, + rewards_share=1, + attestation_duty=DutyAccumulator(assigned=10, included=10), + sync_duty=DutyAccumulator(assigned=10, included=10), + ), + ValidatorIndex(4): ValidatorFrameSummary( + performance=0.12903225806451613, + threshold=0.8842289719626168, + rewards_share=1, + strikes=1, + attestation_duty=DutyAccumulator(assigned=10, included=0), + proposal_duty=DutyAccumulator(assigned=10, included=10), + ), + }, + ), + ), + NodeOperatorId(3): OperatorFrameSummary( + distributed=0, + performance_coefficients=PerformanceCoefficients(), + validators=defaultdict( + ValidatorFrameSummary, + { + ValidatorIndex(5): ValidatorFrameSummary( + performance=0.0, + threshold=0.8842289719626168, + rewards_share=1, + strikes=1, + attestation_duty=DutyAccumulator(assigned=10, included=0), + ), + }, + ), + ), + NodeOperatorId(5): OperatorFrameSummary( + distributed=47, + performance_coefficients=PerformanceCoefficients(), + validators=defaultdict( + ValidatorFrameSummary, + { + ValidatorIndex(7): ValidatorFrameSummary( + performance=1.0, + threshold=0.8842289719626168, + rewards_share=1, + slashed=False, + strikes=0, + attestation_duty=DutyAccumulator(assigned=10, included=10), + proposal_duty=DutyAccumulator(assigned=10, included=10), + ), + ValidatorIndex(8): ValidatorFrameSummary( + performance=1.0, + threshold=0.7842289719626168, + rewards_share=0.9, + slashed=False, + strikes=0, + attestation_duty=DutyAccumulator(assigned=10, included=10), + sync_duty=DutyAccumulator(assigned=10, included=10), + ), + }, + ), + ), + }, + ), + ), + ), + # No duties + ( + 100, + { + (..., NodeOperatorId(1)): [ + LidoValidatorFactory.build( + index=ValidatorIndex(1), validator=ValidatorStateFactory.build(slashed=False) + ), + ], + }, + NetworkDuties(), + Mock( + return_value=CurveParams( + strikes_params=..., + perf_leeway_data=Mock(get_for=Mock(return_value=0.1)), + reward_share_data=Mock(get_for=Mock(return_value=1)), + perf_coeffs=PerformanceCoefficients(), + ) + ), + # Expected: + # Distribution map + {}, + # Distributed rewards + 0, + # Rebate to protocol + 100, + # Strikes + {}, + FramePerfLog( + blockstamp=..., + frame=..., + distributable=100, + distributed_rewards=0, + rebate_to_protocol=100, + operators={}, + ), + ), + ], +) +def test_calculate_distribution_in_frame( + to_distribute, + frame_validators, + frame_state_data, + mocked_curve_params, + expected_rewards_distribution_map, + expected_distributed_rewards, + expected_rebate_to_protocol, + expected_frame_strikes, + expected_log, +): + log = FramePerfLog(blockstamp=..., frame=...) + # Mocking the data from EL + w3 = Mock(spec=Web3, csm=Mock(spec=CSM)) + w3.csm.get_curve_params = mocked_curve_params -def test_calculate_distribution_in_frame_handles_below_threshold_performance(module: CSOracle): - frame = Mock() - rewards_to_distribute = UINT64_MAX - validator = LidoValidatorFactory.build() - validator.validator.slashed = False - node_operator_id = validator.lido_id.operatorIndex - operators_to_validators = {(Mock(), node_operator_id): [validator]} + frame = (EpochNumber(0), EpochNumber(31)) state = State() - attestation_duty = DutyAccumulator(assigned=10, included=5) - proposal_duty = DutyAccumulator(assigned=10, included=5) - sync_duty = DutyAccumulator(assigned=10, included=5) - state.data = { - frame: Duties( - attestations={validator.index: attestation_duty}, - proposals={validator.index: proposal_duty}, - syncs={validator.index: sync_duty}, - ) - } - state.frames = [frame] - blockstamp = Mock() - - log = FramePerfLog(Mock(), frame) - - module.w3.csm.get_curve_params = Mock( - return_value=Mock( - strikes_params=StrikesParams(lifetime=6, threshold=Mock()), - perf_leeway_data=Mock(get_for=Mock(return_value=-0.1)), - reward_share_data=Mock(get_for=Mock(return_value=1)), - perf_coeffs=PerformanceCoefficients(), - ) - ) + state.migrate(*frame, epochs_per_frame=32, consensus_version=3) + state.data = {frame: frame_state_data} + + distribution = Distribution(w3, converter=..., state=state) - distribution = Distribution(module.w3, Mock(), state) - rewards_distribution, distributed_rewards, rebate_to_protocol, strikes_in_frame = ( + (rewards_distribution, distributed_rewards, rebate_to_protocol, strikes_in_frame) = ( distribution._calculate_distribution_in_frame( - frame, blockstamp, rewards_to_distribute, operators_to_validators, log + frame, + blockstamp=..., + rewards_to_distribute=to_distribute, + operators_to_validators=frame_validators, + log=log, ) ) - assert rewards_distribution[node_operator_id] == 0 - assert log.operators[node_operator_id].distributed == 0 - assert log.operators[node_operator_id].validators[validator.index].attestation_duty == attestation_duty - assert log.operators[node_operator_id].validators[validator.index].proposal_duty == proposal_duty - assert log.operators[node_operator_id].validators[validator.index].sync_duty == sync_duty - assert (node_operator_id, validator.pubkey) in strikes_in_frame + assert dict(rewards_distribution) == expected_rewards_distribution_map + assert distributed_rewards == expected_distributed_rewards + assert rebate_to_protocol == expected_rebate_to_protocol + assert strikes_in_frame == expected_frame_strikes + assert log == expected_log -def test_process_validator_duty_handles_above_threshold_performance(): - validator = LidoValidatorFactory.build() - validator.validator.slashed = False - log_operator = Mock() - log_operator.validators = defaultdict(ValidatorFrameSummary) - threshold = 0.5 - reward_share = 1 - - validator_duties = ValidatorDuties( - attestation=DutyAccumulator(assigned=10, included=6), - proposal=DutyAccumulator(assigned=10, included=6), - sync=DutyAccumulator(assigned=10, included=6), - ) - - outcome = Distribution.get_validator_duties_outcome( - validator, - validator_duties, - threshold, - reward_share, - PerformanceCoefficients(), - log_operator, - ) - - assert outcome.strikes == 0 - assert outcome.rebate_share == 0 - assert outcome.participation_share == 10 - assert log_operator.validators[validator.index].attestation_duty == validator_duties.attestation - assert log_operator.validators[validator.index].proposal_duty == validator_duties.proposal - assert log_operator.validators[validator.index].sync_duty == validator_duties.sync - - -def test_process_validator_duty_handles_below_threshold_performance(): - validator = LidoValidatorFactory.build() - validator.validator.slashed = False - log_operator = Mock() - log_operator.validators = defaultdict(ValidatorFrameSummary) - threshold = 0.5 - reward_share = 1 - - validator_duties = ValidatorDuties( - attestation=DutyAccumulator(assigned=10, included=4), - proposal=DutyAccumulator(assigned=10, included=4), - sync=DutyAccumulator(assigned=10, included=4), +@pytest.mark.parametrize( + "att_perf, prop_perf, sync_perf, expected", + [ + (1.0, 1.0, 1.0, 1.0), + (0.0, 0.0, 0.0, 0.0), + (0.5, 0.5, 0.5, 0.5), + (0.9, None, 0.7, pytest.approx(0.8928, rel=1e-4)), + (0.95, None, None, 0.95), + (0.95, 0.5, None, pytest.approx(0.8919, rel=1e-4)), + (0.95, None, 0.7, pytest.approx(0.9410, rel=1e-4)), + (0.95, 0.5, 0.7, pytest.approx(0.8859, rel=1e-4)), + ], +) +def test_get_network_performance(att_perf, prop_perf, sync_perf, expected): + distribution = Distribution(Mock(), Mock(), Mock()) + distribution.state.get_att_network_aggr = Mock(return_value=Mock(perf=att_perf) if att_perf is not None else None) + distribution.state.get_prop_network_aggr = Mock( + return_value=Mock(perf=prop_perf) if prop_perf is not None else None ) - - outcome = Distribution.get_validator_duties_outcome( - validator, - validator_duties, - threshold, - reward_share, - PerformanceCoefficients(), - log_operator, + distribution.state.get_sync_network_aggr = Mock( + return_value=Mock(perf=sync_perf) if sync_perf is not None else None ) + frame = Mock(spec=Frame) - assert outcome.strikes == 1 - assert outcome.rebate_share == 0 - assert outcome.participation_share == 0 - assert log_operator.validators[validator.index].attestation_duty == validator_duties.attestation - assert log_operator.validators[validator.index].proposal_duty == validator_duties.proposal - assert log_operator.validators[validator.index].sync_duty == validator_duties.sync + result = distribution._get_network_performance(frame) + assert result == expected -def test_process_validator_duty_handles_no_duty_assigned(): - validator = LidoValidatorFactory.build() - log_operator = Mock() - log_operator.validators = defaultdict(ValidatorFrameSummary) - threshold = 0.5 - reward_share = 1 - validator_duties = ValidatorDuties(attestation=None, proposal=None, sync=None) +def test_get_network_performance_raises_error_for_invalid_performance(): + distribution = Distribution(Mock(), Mock(), Mock()) + distribution.state.get_att_network_aggr = Mock(return_value=Mock(perf=1.1)) + distribution.state.get_prop_network_aggr = Mock(return_value=Mock(perf=1.0)) + distribution.state.get_sync_network_aggr = Mock(return_value=Mock(perf=1.0)) + frame = Mock(spec=Frame) - outcome = Distribution.get_validator_duties_outcome( - validator, - validator_duties, - threshold, - reward_share, - PerformanceCoefficients(), - log_operator, - ) + with pytest.raises(ValueError, match="Invalid performance: performance"): + distribution._get_network_performance(frame) - assert outcome.strikes == 0 - assert outcome.rebate_share == 0 - assert outcome.participation_share == 0 - assert validator.index not in log_operator.validators - -def test_process_validator_duty_handles_slashed_validator(): +@pytest.mark.parametrize( + "validator_duties, is_slashed, threshold, reward_share, expected_outcome", + [ + ( + ValidatorDuties( + attestation=DutyAccumulator(assigned=10, included=6), + proposal=DutyAccumulator(assigned=10, included=6), + sync=DutyAccumulator(assigned=10, included=6), + ), + False, + 0.5, + 1, + ValidatorDutiesOutcome(participation_share=10, rebate_share=0, strikes=0), + ), + ( + ValidatorDuties( + attestation=DutyAccumulator(assigned=10, included=4), + proposal=DutyAccumulator(assigned=10, included=4), + sync=DutyAccumulator(assigned=10, included=4), + ), + False, + 0.5, + 1, + ValidatorDutiesOutcome(participation_share=0, rebate_share=0, strikes=1), + ), + ( + ValidatorDuties(attestation=None, proposal=None, sync=None), + False, + 0.5, + 1, + ValidatorDutiesOutcome(participation_share=0, rebate_share=0, strikes=0), + ), + ( + ValidatorDuties( + attestation=DutyAccumulator(assigned=1, included=1), + proposal=DutyAccumulator(assigned=1, included=1), + sync=DutyAccumulator(assigned=1, included=1), + ), + True, + 0.5, + 1, + ValidatorDutiesOutcome(participation_share=0, rebate_share=0, strikes=1), + ), + ], +) +def test_process_validator_duty(validator_duties, is_slashed, threshold, reward_share, expected_outcome): validator = LidoValidatorFactory.build() - validator.validator.slashed = True + validator.validator.slashed = is_slashed log_operator = Mock() log_operator.validators = defaultdict(ValidatorFrameSummary) - threshold = 0.5 - reward_share = 1 - - validator_duties = ValidatorDuties( - attestation=DutyAccumulator(assigned=1, included=1), - proposal=DutyAccumulator(assigned=1, included=1), - sync=DutyAccumulator(assigned=1, included=1), - ) outcome = Distribution.get_validator_duties_outcome( validator, @@ -413,61 +911,65 @@ def test_process_validator_duty_handles_slashed_validator(): log_operator, ) - assert outcome.strikes == 1 - assert outcome.rebate_share == 0 - assert outcome.participation_share == 0 - assert log_operator.validators[validator.index].slashed is True - - -def test_calc_rewards_distribution_in_frame_correctly_distributes_rewards(): - participation_shares = {NodeOperatorId(1): 100, NodeOperatorId(2): 200} - rewards_to_distribute = Wei(1 * 10**18) - rebate_share = 0 - - rewards_distribution = Distribution.calc_rewards_distribution_in_frame( - participation_shares, rebate_share, rewards_to_distribute - ) - - assert rewards_distribution[NodeOperatorId(1)] == Wei(333333333333333333) - assert rewards_distribution[NodeOperatorId(2)] == Wei(666666666666666666) + assert outcome == expected_outcome + if validator_duties.attestation and not is_slashed: + assert log_operator.validators[validator.index].threshold == threshold + assert log_operator.validators[validator.index].rewards_share == reward_share + if validator_duties.attestation: + assert log_operator.validators[validator.index].attestation_duty == validator_duties.attestation + if validator_duties.proposal: + assert log_operator.validators[validator.index].proposal_duty == validator_duties.proposal + if validator_duties.sync: + assert log_operator.validators[validator.index].sync_duty == validator_duties.sync + if not validator_duties.attestation: + assert validator.index not in log_operator.validators -def test_calc_rewards_distribution_in_frame_handles_zero_participation(): - participation_shares = {NodeOperatorId(1): 0, NodeOperatorId(2): 0} - rewards_to_distribute = Wei(1 * 10**18) - rebate_share = 0 - - rewards_distribution = Distribution.calc_rewards_distribution_in_frame( - participation_shares, rebate_share, rewards_to_distribute - ) - - assert rewards_distribution[NodeOperatorId(1)] == 0 - assert rewards_distribution[NodeOperatorId(2)] == 0 - - -def test_calc_rewards_distribution_in_frame_handles_no_participation(): - participation_shares = {} - rewards_to_distribute = Wei(1 * 10**18) - rebate_share = 0 - - rewards_distribution = Distribution.calc_rewards_distribution_in_frame( - participation_shares, rebate_share, rewards_to_distribute - ) - - assert len(rewards_distribution) == 0 + assert log_operator.validators[validator.index].slashed is is_slashed -def test_calc_rewards_distribution_in_frame_handles_partial_participation(): - participation_shares = {NodeOperatorId(1): 100, NodeOperatorId(2): 0} - rewards_to_distribute = Wei(1 * 10**18) - rebate_share = 0 - +@pytest.mark.parametrize( + "participation_shares, rewards_to_distribute, rebate_share, expected_distribution", + [ + ( + {NodeOperatorId(1): 100, NodeOperatorId(2): 200}, + Wei(1 * 10**18), + 0, + {NodeOperatorId(1): Wei(333333333333333333), NodeOperatorId(2): Wei(666666666666666666)}, + ), + ( + {NodeOperatorId(1): 0, NodeOperatorId(2): 0}, + Wei(1 * 10**18), + 0, + {}, + ), + ( + {}, + Wei(1 * 10**18), + 0, + {}, + ), + ( + {NodeOperatorId(1): 100, NodeOperatorId(2): 0}, + Wei(1 * 10**18), + 0, + {NodeOperatorId(1): Wei(1 * 10**18)}, + ), + ( + {NodeOperatorId(1): 100, NodeOperatorId(2): 200}, + Wei(1 * 10**18), + 10, + {NodeOperatorId(1): Wei(322580645161290322), NodeOperatorId(2): Wei(645161290322580645)}, + ), + ], +) +def test_calc_rewards_distribution_in_frame( + participation_shares, rewards_to_distribute, rebate_share, expected_distribution +): rewards_distribution = Distribution.calc_rewards_distribution_in_frame( participation_shares, rebate_share, rewards_to_distribute ) - - assert rewards_distribution[NodeOperatorId(1)] == Wei(1 * 10**18) - assert rewards_distribution[NodeOperatorId(2)] == 0 + assert rewards_distribution == expected_distribution def test_calc_rewards_distribution_in_frame_handles_negative_to_distribute(): @@ -503,6 +1005,7 @@ def test_calc_rewards_distribution_in_frame_handles_negative_to_distribute(): { (NodeOperatorId(42), b"00"): StrikesList([3, 0, 0, 0, 0, 0]), (NodeOperatorId(17), b"01"): StrikesList([1, 0, 0, 0]), + (NodeOperatorId(17), b"02"): StrikesList([0, 0, 0, 1]), }, {}, { @@ -539,13 +1042,12 @@ def test_calc_rewards_distribution_in_frame_handles_negative_to_distribute(): ], ) def test_merge_strikes( - module: CSOracle, acc: dict, strikes_in_frame: dict, threshold_per_op: dict, expected: dict, ): - distribution = Distribution(module.w3, Mock(), Mock()) + distribution = Distribution(Mock(csm=Mock()), Mock(), Mock()) distribution.w3.csm.get_curve_params = Mock( side_effect=lambda no_id, _: Mock(strikes_params=threshold_per_op[no_id]) ) @@ -553,3 +1055,104 @@ def test_merge_strikes( distribution._merge_strikes(acc, strikes_in_frame, frame_blockstamp=Mock()) assert acc == expected + + +@pytest.mark.parametrize( + "total_distributed_rewards, total_rebate, total_rewards_to_distribute", + [ + (100, 50, 150), + (0, 0, 0), + (50, 50, 100), + ], +) +def tests_validates_correct_distribution(total_distributed_rewards, total_rebate, total_rewards_to_distribute): + Distribution.validate_distribution(total_distributed_rewards, total_rebate, total_rewards_to_distribute) + + +@pytest.mark.parametrize( + "total_distributed_rewards, total_rebate, total_rewards_to_distribute", + [ + (100, 51, 150), + (100, 50, 149), + (200, 0, 199), + ], +) +def test_raises_error_for_invalid_distribution(total_distributed_rewards, total_rebate, total_rewards_to_distribute): + with pytest.raises(ValueError, match="Invalid distribution"): + Distribution.validate_distribution(total_distributed_rewards, total_rebate, total_rewards_to_distribute) + + +@pytest.mark.parametrize( + "attestation_perf, proposal_perf, sync_perf, expected", + [ + (0.95, None, None, 0.95), + (0.95, 0.5, None, pytest.approx(0.8919, rel=1e-4)), + (0.95, None, 0.7, pytest.approx(0.9410, rel=1e-4)), + (0.95, 0.5, 0.7, pytest.approx(0.8859, rel=1e-4)), + (1, 1, 1, 1), + ], +) +def test_performance_coefficients_calc_performance(attestation_perf, proposal_perf, sync_perf, expected): + performance_coefficients = PerformanceCoefficients() + duties = ValidatorDuties( + attestation=Mock(perf=attestation_perf), + proposal=Mock(perf=proposal_perf) if proposal_perf is not None else None, + sync=Mock(perf=sync_perf) if sync_perf is not None else None, + ) + assert performance_coefficients.calc_performance(duties) == expected + + +@pytest.mark.parametrize( + "key_pivots, performance_leeway, key_number, expected", + [ + ([], [10000], 100500, 10000 / TOTAL_BASIS_POINTS), + ([1], [10000, 9000], 1, 10000 / TOTAL_BASIS_POINTS), + ([1], [10000, 9000], 2, 9000 / TOTAL_BASIS_POINTS), + ([10, 20], [1000, 2000, 3000], 5, 1000 / TOTAL_BASIS_POINTS), + ([10, 20], [1000, 2000, 3000], 15, 2000 / TOTAL_BASIS_POINTS), + ([10, 20], [1000, 2000, 3000], 25, 3000 / TOTAL_BASIS_POINTS), + ], +) +def test_performance_leeway_returns_correct_reward_share(key_pivots, performance_leeway, key_number, expected): + performance_leeway = PerformanceLeeway(key_pivots, performance_leeway) + assert performance_leeway.get_for(key_number) == expected + + +def test_performance_leeway_raises_error_for_invalid_key_number(): + performance_leeway = PerformanceLeeway([10, 20], [1000, 2000, 3000]) + with pytest.raises(ValueError, match="Key number should be greater than 0"): + performance_leeway.get_for(0) + + +def test_performance_leeway_raises_error_for_key_number_out_of_range(): + performance_leeway = PerformanceLeeway([10], [10000]) + with pytest.raises(ValueError, match="Can't find performance leeway for key_number=40"): + performance_leeway.get_for(40) + + +@pytest.mark.parametrize( + "key_pivots, reward_shares, key_number, expected", + [ + ([], [10000], 100500, 10000 / TOTAL_BASIS_POINTS), + ([1], [10000, 9000], 1, 10000 / TOTAL_BASIS_POINTS), + ([1], [10000, 9000], 2, 9000 / TOTAL_BASIS_POINTS), + ([10, 20], [1000, 2000, 3000], 5, 1000 / TOTAL_BASIS_POINTS), + ([10, 20], [1000, 2000, 3000], 15, 2000 / TOTAL_BASIS_POINTS), + ([10, 20], [1000, 2000, 3000], 25, 3000 / TOTAL_BASIS_POINTS), + ], +) +def test_reward_share_returns_correct_reward_share(key_pivots, reward_shares, key_number, expected): + reward_share = RewardShare(key_pivots, reward_shares) + assert reward_share.get_for(key_number) == expected + + +def test_reward_share_raises_error_for_invalid_key_number(): + reward_share = RewardShare([10, 20], [1000, 2000, 3000]) + with pytest.raises(ValueError, match="Key number should be greater than 0"): + reward_share.get_for(0) + + +def test_reward_share_raises_error_for_key_number_out_of_range(): + reward_share = RewardShare([10], [10000]) + with pytest.raises(ValueError, match="Can't find performance leeway for key_number=40"): + reward_share.get_for(40) diff --git a/tests/modules/csm/test_state.py b/tests/modules/csm/test_state.py index c7d2a2015..703bd9d00 100644 --- a/tests/modules/csm/test_state.py +++ b/tests/modules/csm/test_state.py @@ -7,7 +7,7 @@ import pytest from src import variables -from src.modules.csm.state import State, InvalidState, DutyAccumulator, Duties +from src.modules.csm.state import State, InvalidState, DutyAccumulator, NetworkDuties from src.types import ValidatorIndex from src.utils.range import sequence @@ -80,7 +80,7 @@ def test_is_empty_returns_true_for_empty_state(): def test_is_empty_returns_false_for_non_empty_state(): state = State() - state.data = {(0, 31): Duties()} + state.data = {(0, 31): NetworkDuties()} assert not state.is_empty @@ -149,7 +149,7 @@ def test_increment_att_duty_adds_duty_correctly(): state.frames = [frame] duty_epoch, _ = frame state.data = { - frame: Duties(attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})), + frame: NetworkDuties(attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})), } state.increment_att_duty(duty_epoch, ValidatorIndex(1), True) assert state.data[frame].attestations[ValidatorIndex(1)].assigned == 11 @@ -162,7 +162,7 @@ def test_increment_prop_duty_adds_duty_correctly(): state.frames = [frame] duty_epoch, _ = frame state.data = { - frame: Duties(proposals=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})), + frame: NetworkDuties(proposals=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})), } state.increment_prop_duty(duty_epoch, ValidatorIndex(1), True) assert state.data[frame].proposals[ValidatorIndex(1)].assigned == 11 @@ -175,7 +175,7 @@ def test_increment_sync_duty_adds_duty_correctly(): state.frames = [frame] duty_epoch, _ = frame state.data = { - frame: Duties(syncs=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})), + frame: NetworkDuties(syncs=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})), } state.increment_sync_duty(duty_epoch, ValidatorIndex(1), True) assert state.data[frame].syncs[ValidatorIndex(1)].assigned == 11 @@ -188,7 +188,7 @@ def test_increment_att_duty_creates_new_validator_entry(): state.frames = [frame] duty_epoch, _ = frame state.data = { - frame: Duties(), + frame: NetworkDuties(), } state.increment_att_duty(duty_epoch, ValidatorIndex(2), True) assert state.data[frame].attestations[ValidatorIndex(2)].assigned == 1 @@ -201,7 +201,7 @@ def test_increment_prop_duty_creates_new_validator_entry(): state.frames = [frame] duty_epoch, _ = frame state.data = { - frame: Duties(), + frame: NetworkDuties(), } state.increment_prop_duty(duty_epoch, ValidatorIndex(2), True) assert state.data[frame].proposals[ValidatorIndex(2)].assigned == 1 @@ -214,7 +214,7 @@ def test_increment_sync_duty_creates_new_validator_entry(): state.frames = [frame] duty_epoch, _ = frame state.data = { - frame: Duties(), + frame: NetworkDuties(), } state.increment_sync_duty(duty_epoch, ValidatorIndex(2), True) assert state.data[frame].syncs[ValidatorIndex(2)].assigned == 1 @@ -227,7 +227,7 @@ def test_increment_att_duty_handles_non_included_duty(): state.frames = [frame] duty_epoch, _ = frame state.data = { - frame: Duties(attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})), + frame: NetworkDuties(attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})), } state.increment_att_duty(duty_epoch, ValidatorIndex(1), False) assert state.data[frame].attestations[ValidatorIndex(1)].assigned == 11 @@ -240,7 +240,7 @@ def test_increment_prop_duty_handles_non_included_duty(): state.frames = [frame] duty_epoch, _ = frame state.data = { - frame: Duties(proposals=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})), + frame: NetworkDuties(proposals=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})), } state.increment_prop_duty(duty_epoch, ValidatorIndex(1), False) assert state.data[frame].proposals[ValidatorIndex(1)].assigned == 11 @@ -253,7 +253,7 @@ def test_increment_sync_duty_handles_non_included_duty(): state.frames = [frame] duty_epoch, _ = frame state.data = { - frame: Duties(syncs=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})), + frame: NetworkDuties(syncs=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})), } state.increment_sync_duty(duty_epoch, ValidatorIndex(1), False) assert state.data[frame].syncs[ValidatorIndex(1)].assigned == 11 @@ -343,12 +343,12 @@ def test_migrate_migrates_data(): state._consensus_version = 1 state.frames = [(0, 31), (32, 63)] state.data = { - (0, 31): Duties( + (0, 31): NetworkDuties( attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), ), - (32, 63): Duties( + (32, 63): NetworkDuties( attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(20, 15)}), proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(20, 15)}), syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(20, 15)}), @@ -358,7 +358,7 @@ def test_migrate_migrates_data(): state.migrate(0, 63, 64, 1) assert state.data == { - (0, 63): Duties( + (0, 63): NetworkDuties( attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(30, 20)}), proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(30, 20)}), syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(30, 20)}), @@ -375,7 +375,7 @@ def test_migrate_invalidates_unmigrated_frames(): state._consensus_version = 1 state.frames = [(0, 63)] state.data = { - (0, 63): Duties( + (0, 63): NetworkDuties( attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(30, 20)}), proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(30, 20)}), syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(30, 20)}), @@ -385,7 +385,7 @@ def test_migrate_invalidates_unmigrated_frames(): state.migrate(0, 31, 32, 1) assert state.data == { - (0, 31): Duties(), + (0, 31): NetworkDuties(), } assert state._processed_epochs == set() assert state.frames == [(0, 31)] @@ -399,17 +399,17 @@ def test_migrate_discards_unmigrated_frame(): state._consensus_version = 1 state.frames = [(0, 31), (32, 63), (64, 95)] state.data = { - (0, 31): Duties( + (0, 31): NetworkDuties( attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), ), - (32, 63): Duties( + (32, 63): NetworkDuties( attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(20, 15)}), proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(20, 15)}), syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(20, 15)}), ), - (64, 95): Duties( + (64, 95): NetworkDuties( attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(30, 25)}), proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(30, 25)}), syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(30, 25)}), @@ -420,12 +420,12 @@ def test_migrate_discards_unmigrated_frame(): state.migrate(0, 63, 32, 1) assert state.data == { - (0, 31): Duties( + (0, 31): NetworkDuties( attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), ), - (32, 63): Duties( + (32, 63): NetworkDuties( attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(20, 15)}), proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(20, 15)}), syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(20, 15)}), @@ -443,12 +443,12 @@ def test_migrate_frames_data_creates_new_data_correctly(): state.frames = [(0, 31), (32, 63)] new_frames = [(0, 63)] state.data = { - (0, 31): Duties( + (0, 31): NetworkDuties( attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), ), - (32, 63): Duties( + (32, 63): NetworkDuties( attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(20, 15)}), proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(20, 15)}), syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(20, 15)}), @@ -459,7 +459,7 @@ def test_migrate_frames_data_creates_new_data_correctly(): state._migrate_frames_data(new_frames) assert state.data == { - (0, 63): Duties( + (0, 63): NetworkDuties( attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(30, 20)}), proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(30, 20)}), syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(30, 20)}), @@ -473,7 +473,7 @@ def test_migrate_frames_data_handles_no_migration(): state.frames = [(0, 31)] new_frames = [(0, 31)] state.data = { - (0, 31): Duties( + (0, 31): NetworkDuties( attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), @@ -484,7 +484,7 @@ def test_migrate_frames_data_handles_no_migration(): state._migrate_frames_data(new_frames) assert state.data == { - (0, 31): Duties( + (0, 31): NetworkDuties( attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), @@ -498,12 +498,12 @@ def test_migrate_frames_data_handles_partial_migration(): state.frames = [(0, 31), (32, 63)] new_frames = [(0, 31), (32, 95)] state.data = { - (0, 31): Duties( + (0, 31): NetworkDuties( attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), ), - (32, 63): Duties( + (32, 63): NetworkDuties( attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(20, 15)}), proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(20, 15)}), syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(20, 15)}), @@ -514,12 +514,12 @@ def test_migrate_frames_data_handles_partial_migration(): state._migrate_frames_data(new_frames) assert state.data == { - (0, 31): Duties( + (0, 31): NetworkDuties( attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(10, 5)}), syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(10, 5)}), ), - (32, 95): Duties( + (32, 95): NetworkDuties( attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(20, 15)}), proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(20, 15)}), syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(20, 15)}), @@ -532,11 +532,11 @@ def test_migrate_frames_data_handles_no_data(): state = State() state.frames = [(0, 31)] new_frames = [(0, 31)] - state.data = {frame: Duties() for frame in state.frames} + state.data = {frame: NetworkDuties() for frame in state.frames} state._migrate_frames_data(new_frames) - assert state.data == {(0, 31): Duties()} + assert state.data == {(0, 31): NetworkDuties()} def test_migrate_frames_data_handles_wider_old_frame(): @@ -544,7 +544,7 @@ def test_migrate_frames_data_handles_wider_old_frame(): state.frames = [(0, 63)] new_frames = [(0, 31), (32, 63)] state.data = { - (0, 63): Duties( + (0, 63): NetworkDuties( attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(30, 20)}), proposals=defaultdict(DutyAccumulator, {ValidatorIndex(2): DutyAccumulator(30, 20)}), syncs=defaultdict(DutyAccumulator, {ValidatorIndex(3): DutyAccumulator(30, 20)}), @@ -555,8 +555,8 @@ def test_migrate_frames_data_handles_wider_old_frame(): state._migrate_frames_data(new_frames) assert state.data == { - (0, 31): Duties(), - (32, 63): Duties(), + (0, 31): NetworkDuties(), + (32, 63): NetworkDuties(), } assert state._processed_epochs == set() @@ -598,10 +598,37 @@ def test_attestation_aggregate_perf(): assert aggr.perf == pytest.approx(0.4285, abs=1e-4) +def test_get_validator_duties(): + state = State() + state.data = { + (0, 31): NetworkDuties( + attestations=defaultdict( + DutyAccumulator, + {ValidatorIndex(1): DutyAccumulator(10, 5), ValidatorIndex(2): DutyAccumulator(20, 15)}, + ), + proposals=defaultdict( + DutyAccumulator, + {ValidatorIndex(1): DutyAccumulator(7, 1), ValidatorIndex(2): DutyAccumulator(20, 15)}, + ), + syncs=defaultdict( + DutyAccumulator, + {ValidatorIndex(1): DutyAccumulator(3, 2), ValidatorIndex(2): DutyAccumulator(20, 15)}, + ), + ) + } + duties = state.get_validator_duties((0, 31), ValidatorIndex(1)) + assert duties.attestation.assigned == 10 + assert duties.attestation.included == 5 + assert duties.proposal.assigned == 7 + assert duties.proposal.included == 1 + assert duties.sync.assigned == 3 + assert duties.sync.included == 2 + + def test_get_att_network_aggr_computes_correctly(): state = State() state.data = { - (0, 31): Duties( + (0, 31): NetworkDuties( attestations=defaultdict( DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5), ValidatorIndex(2): DutyAccumulator(20, 15)}, @@ -616,7 +643,7 @@ def test_get_att_network_aggr_computes_correctly(): def test_get_sync_network_aggr_computes_correctly(): state = State() state.data = { - (0, 31): Duties( + (0, 31): NetworkDuties( syncs=defaultdict( DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5), ValidatorIndex(2): DutyAccumulator(20, 15)}, @@ -631,7 +658,7 @@ def test_get_sync_network_aggr_computes_correctly(): def test_get_prop_network_aggr_computes_correctly(): state = State() state.data = { - (0, 31): Duties( + (0, 31): NetworkDuties( proposals=defaultdict( DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5), ValidatorIndex(2): DutyAccumulator(20, 15)}, @@ -646,7 +673,7 @@ def test_get_prop_network_aggr_computes_correctly(): def test_get_att_network_aggr_raises_error_for_invalid_accumulator(): state = State() state.data = { - (0, 31): Duties(attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 15)})) + (0, 31): NetworkDuties(attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 15)})) } with pytest.raises(ValueError, match="Invalid accumulator"): state.get_att_network_aggr((0, 31)) @@ -654,14 +681,18 @@ def test_get_att_network_aggr_raises_error_for_invalid_accumulator(): def test_get_prop_network_aggr_raises_error_for_invalid_accumulator(): state = State() - state.data = {(0, 31): Duties(proposals=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 15)}))} + state.data = { + (0, 31): NetworkDuties(proposals=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 15)})) + } with pytest.raises(ValueError, match="Invalid accumulator"): state.get_prop_network_aggr((0, 31)) def test_get_sync_network_aggr_raises_error_for_invalid_accumulator(): state = State() - state.data = {(0, 31): Duties(syncs=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 15)}))} + state.data = { + (0, 31): NetworkDuties(syncs=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 15)})) + } with pytest.raises(ValueError, match="Invalid accumulator"): state.get_sync_network_aggr((0, 31)) @@ -686,7 +717,7 @@ def test_get_sync_network_aggr_raises_error_for_missing_frame_data(): def test_get_att_network_aggr_handles_empty_frame_data(): state = State() - state.data = {(0, 31): Duties()} + state.data = {(0, 31): NetworkDuties()} aggr = state.get_att_network_aggr((0, 31)) assert aggr.assigned == 0 assert aggr.included == 0 @@ -694,7 +725,7 @@ def test_get_att_network_aggr_handles_empty_frame_data(): def test_get_prop_network_aggr_handles_empty_frame_data(): state = State() - state.data = {(0, 31): Duties()} + state.data = {(0, 31): NetworkDuties()} aggr = state.get_prop_network_aggr((0, 31)) assert aggr.assigned == 0 assert aggr.included == 0 @@ -702,7 +733,7 @@ def test_get_prop_network_aggr_handles_empty_frame_data(): def test_get_sync_network_aggr_handles_empty_frame_data(): state = State() - state.data = {(0, 31): Duties()} + state.data = {(0, 31): NetworkDuties()} aggr = state.get_sync_network_aggr((0, 31)) assert aggr.assigned == 0 assert aggr.included == 0 From ed751eca479eaed75427ab9d7bdd9181ac6f1189 Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Thu, 6 Mar 2025 15:43:43 +0100 Subject: [PATCH 096/162] test: add smoke tests for CSM contracts --- .../execution/contracts/cs_fee_oracle.py | 13 ---- tests/integration/conftest.py | 67 ++++++++++++++++++- tests/integration/contracts/contract_utils.py | 27 ++++++-- .../contracts/test_cs_accounting.py | 16 +++++ .../contracts/test_cs_fee_distributor.py | 19 ++++++ .../contracts/test_cs_fee_oracle.py | 15 +++++ tests/integration/contracts/test_cs_module.py | 17 +++++ .../contracts/test_cs_parameters_registry.py | 24 +++++++ .../integration/contracts/test_cs_strikes.py | 17 +++++ 9 files changed, 196 insertions(+), 19 deletions(-) create mode 100644 tests/integration/contracts/test_cs_accounting.py create mode 100644 tests/integration/contracts/test_cs_fee_distributor.py create mode 100644 tests/integration/contracts/test_cs_fee_oracle.py create mode 100644 tests/integration/contracts/test_cs_module.py create mode 100644 tests/integration/contracts/test_cs_parameters_registry.py create mode 100644 tests/integration/contracts/test_cs_strikes.py diff --git a/src/providers/execution/contracts/cs_fee_oracle.py b/src/providers/execution/contracts/cs_fee_oracle.py index e61d681e9..824a76f6b 100644 --- a/src/providers/execution/contracts/cs_fee_oracle.py +++ b/src/providers/execution/contracts/cs_fee_oracle.py @@ -25,19 +25,6 @@ def is_paused(self, block_identifier: BlockIdentifier = "latest") -> bool: ) return resp - def perf_leeway_bp(self, block_identifier: BlockIdentifier = "latest") -> int: - """Performance threshold leeway used to determine underperforming validators""" - - resp = self.functions.avgPerfLeewayBP().call(block_identifier=block_identifier) - logger.info( - { - "msg": "Call `avgPerfLeewayBP()`.", - "value": resp, - "block_identifier": repr(block_identifier), - } - ) - return resp - def strikes(self, block_identifier: BlockIdentifier = "latest") -> ChecksumAddress: """Return the address of the CSStrikes contract""" diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 13ccd13f8..836e3a7f2 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,11 +1,17 @@ from typing import cast import pytest -from web3 import Web3, HTTPProvider +from web3 import HTTPProvider, Web3 from src import variables from src.providers.execution.contracts.accounting_oracle import AccountingOracleContract from src.providers.execution.contracts.burner import BurnerContract +from src.providers.execution.contracts.cs_accounting import CSAccountingContract +from src.providers.execution.contracts.cs_fee_distributor import CSFeeDistributorContract +from src.providers.execution.contracts.cs_fee_oracle import CSFeeOracleContract +from src.providers.execution.contracts.cs_module import CSModuleContract +from src.providers.execution.contracts.cs_parameters_registry import CSParametersRegistryContract +from src.providers.execution.contracts.cs_strikes import CSStrikesContract from src.providers.execution.contracts.exit_bus_oracle import ExitBusOracleContract from src.providers.execution.contracts.lido import LidoContract from src.providers.execution.contracts.lido_locator import LidoLocatorContract @@ -117,3 +123,62 @@ def burner_contract(web3_provider_integration, lido_locator_contract): BurnerContract, lido_locator_contract.burner(), ) + + +# ╔══════════════════════════════════════════════════════════════════════════════════════════════════╗ +# ║ CSM contracts ║ +# ╚══════════════════════════════════════════════════════════════════════════════════════════════════╝ + + +@pytest.fixture +def cs_module_contract(web3_provider_integration): + return get_contract( + web3_provider_integration, + CSModuleContract, + variables.CSM_MODULE_ADDRESS, + ) + + +@pytest.fixture +def cs_accounting_contract(web3_provider_integration, cs_module_contract): + return get_contract( + web3_provider_integration, + CSAccountingContract, + cs_module_contract.accounting(), + ) + + +@pytest.fixture +def cs_params_contract(web3_provider_integration, cs_module_contract): + return get_contract( + web3_provider_integration, + CSParametersRegistryContract, + cs_module_contract.parameters_registry(), + ) + + +@pytest.fixture +def cs_fee_distributor_contract(web3_provider_integration, cs_accounting_contract): + return get_contract( + web3_provider_integration, + CSFeeDistributorContract, + cs_accounting_contract.fee_distributor(), + ) + + +@pytest.fixture +def cs_fee_oracle_contract(web3_provider_integration, cs_fee_distributor_contract): + return get_contract( + web3_provider_integration, + CSFeeOracleContract, + cs_fee_distributor_contract.oracle(), + ) + + +@pytest.fixture +def cs_strikes_contract(web3_provider_integration, cs_fee_oracle_contract): + return get_contract( + web3_provider_integration, + CSStrikesContract, + cs_fee_oracle_contract.strikes(), + ) diff --git a/tests/integration/contracts/contract_utils.py b/tests/integration/contracts/contract_utils.py index f0bda942a..40344e2c0 100644 --- a/tests/integration/contracts/contract_utils.py +++ b/tests/integration/contracts/contract_utils.py @@ -1,17 +1,23 @@ import logging import re -from typing import Any, Callable +from typing import Any, Callable, Type -from src.providers.execution.base_interface import ContractInterface +from eth_typing import Address, ChecksumAddress +from src.providers.execution.base_interface import ContractInterface +from src.utils.types import hex_str_to_bytes HASH_REGREX = re.compile(r'^0x[0-9,A-F]{64}$', flags=re.IGNORECASE) ADDRESS_REGREX = re.compile('^0x[0-9,A-F]{40}$', flags=re.IGNORECASE) +type FuncName = str +type FuncArgs = tuple +type FuncResp = Any + def check_contract( contract: ContractInterface, - functions_spec: list[tuple[str, tuple | None, Callable[[Any], None]]], + functions_spec: list[tuple[FuncName, FuncArgs | None, Callable[[FuncResp], None]]], caplog, ): caplog.set_level(logging.DEBUG) @@ -33,5 +39,16 @@ def check_value_re(regrex, value) -> None: assert regrex.findall(value) -def check_value_type(value, _type) -> None: - assert isinstance(value, _type) +def check_is_instance_of(type_: Type) -> Callable[[FuncArgs], None]: + if type_ is Address or type_ is ChecksumAddress: + return check_is_address + return lambda resp: check_value_type(resp, type_) + + +def check_value_type(value, type_) -> None: + assert isinstance(value, type_), f"Got invalid type={type(value)}, expected={repr(type_)}" + + +def check_is_address(resp: FuncResp) -> None: + assert isinstance(resp, str), "address should be returned as a string" + assert len(hex_str_to_bytes(resp)) == 20, "Got invalid address length" diff --git a/tests/integration/contracts/test_cs_accounting.py b/tests/integration/contracts/test_cs_accounting.py new file mode 100644 index 000000000..46ae7f5be --- /dev/null +++ b/tests/integration/contracts/test_cs_accounting.py @@ -0,0 +1,16 @@ +import pytest +from eth_typing import Address + +from tests.integration.contracts.contract_utils import check_contract, check_is_instance_of + + +@pytest.mark.integration +def test_cs_accounting(cs_accounting_contract, caplog): + check_contract( + cs_accounting_contract, + [ + ("fee_distributor", None, check_is_instance_of(Address)), + ("get_bond_curve_id", (0,), check_is_instance_of(int)), + ], + caplog, + ) diff --git a/tests/integration/contracts/test_cs_fee_distributor.py b/tests/integration/contracts/test_cs_fee_distributor.py new file mode 100644 index 000000000..3821bd8e4 --- /dev/null +++ b/tests/integration/contracts/test_cs_fee_distributor.py @@ -0,0 +1,19 @@ +import pytest +from eth_typing import Address +from hexbytes import HexBytes + +from tests.integration.contracts.contract_utils import check_contract, check_is_instance_of + + +@pytest.mark.integration +def test_cs_fee_distributor(cs_fee_distributor_contract, caplog): + check_contract( + cs_fee_distributor_contract, + [ + ("oracle", None, check_is_instance_of(Address)), + ("shares_to_distribute", None, check_is_instance_of(int)), + ("tree_root", None, check_is_instance_of(HexBytes)), + ("tree_cid", None, check_is_instance_of(str)), + ], + caplog, + ) diff --git a/tests/integration/contracts/test_cs_fee_oracle.py b/tests/integration/contracts/test_cs_fee_oracle.py new file mode 100644 index 000000000..0504ac8b3 --- /dev/null +++ b/tests/integration/contracts/test_cs_fee_oracle.py @@ -0,0 +1,15 @@ +import pytest + +from tests.integration.contracts.contract_utils import check_contract, check_is_instance_of + + +@pytest.mark.integration +def test_cs_fee_oracle(cs_fee_oracle_contract, caplog): + check_contract( + cs_fee_oracle_contract, + [ + # ("strikes", None, check_is_instance_of(Address)), FIXME: Uncomment with CSMv2 live on mainnet. + ("is_paused", None, check_is_instance_of(bool)), + ], + caplog, + ) diff --git a/tests/integration/contracts/test_cs_module.py b/tests/integration/contracts/test_cs_module.py new file mode 100644 index 000000000..37fab3a45 --- /dev/null +++ b/tests/integration/contracts/test_cs_module.py @@ -0,0 +1,17 @@ +import pytest +from eth_typing import Address + +from tests.integration.contracts.contract_utils import check_contract, check_is_instance_of + + +@pytest.mark.integration +def test_cs_module(cs_module_contract, caplog): + check_contract( + cs_module_contract, + [ + # ("parameters_registry", None, check_is_address), + ("accounting", None, check_is_instance_of(Address)), + ("is_paused", None, check_is_instance_of(bool)), + ], + caplog, + ) diff --git a/tests/integration/contracts/test_cs_parameters_registry.py b/tests/integration/contracts/test_cs_parameters_registry.py new file mode 100644 index 000000000..39e4d5e34 --- /dev/null +++ b/tests/integration/contracts/test_cs_parameters_registry.py @@ -0,0 +1,24 @@ +import pytest + +from src.providers.execution.contracts.cs_parameters_registry import ( + PerformanceCoefficients, + PerformanceLeeway, + RewardShare, + StrikesParams, +) +from tests.integration.contracts.contract_utils import check_contract, check_is_instance_of + + +@pytest.mark.integration +@pytest.mark.skip("Requires CSMv2 activated on mainnet") # TODO: Remove the mark with CSM v2 live on mainnet +def test_cs_parameters_registry(cs_params_contract, caplog): + check_contract( + cs_params_contract, + [ + ("get_performance_coefficients", None, check_is_instance_of(PerformanceCoefficients)), + ("get_reward_share_data", None, check_is_instance_of(RewardShare)), + ("get_performance_leeway_data", None, check_is_instance_of(PerformanceLeeway)), + ("get_strikes_params", None, check_is_instance_of(StrikesParams)), + ], + caplog, + ) diff --git a/tests/integration/contracts/test_cs_strikes.py b/tests/integration/contracts/test_cs_strikes.py new file mode 100644 index 000000000..bb289082f --- /dev/null +++ b/tests/integration/contracts/test_cs_strikes.py @@ -0,0 +1,17 @@ +import pytest +from hexbytes import HexBytes + +from tests.integration.contracts.contract_utils import check_contract, check_is_instance_of + + +@pytest.mark.integration +@pytest.mark.skip("Requires CSMv2 activated on mainnet") # TODO: Remove the mark with CSM v2 live on mainnet +def test_cs_strikes(cs_module_contract, caplog): + check_contract( + cs_module_contract, + [ + ("tree_root", None, check_is_instance_of(HexBytes)), + ("tree_cid", None, check_is_instance_of(str)), + ], + caplog, + ) From f5ef0a42fb44d5ee9737556943564b7fe431537b Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Thu, 6 Mar 2025 16:03:25 +0100 Subject: [PATCH 097/162] refactor: use new check_is_instance_of function --- tests/integration/contracts/contract_utils.py | 13 ++-- .../contracts/test_accounting_oracle.py | 8 +-- tests/integration/contracts/test_bunker.py | 4 +- .../contracts/test_cs_accounting.py | 4 +- .../contracts/test_cs_fee_distributor.py | 4 +- tests/integration/contracts/test_cs_module.py | 4 +- tests/integration/contracts/test_lido.py | 12 ++-- .../contracts/test_lido_locator.py | 72 +++---------------- .../contracts/test_oracle_daemon_config.py | 40 +++-------- .../test_oracle_report_sanity_checker.py | 4 +- .../test_validator_exit_bus_oracle.py | 11 +-- .../test_withdrawal_queue_nft_contract.py | 18 ++--- 12 files changed, 61 insertions(+), 133 deletions(-) diff --git a/tests/integration/contracts/contract_utils.py b/tests/integration/contracts/contract_utils.py index 40344e2c0..3c2a7a4b4 100644 --- a/tests/integration/contracts/contract_utils.py +++ b/tests/integration/contracts/contract_utils.py @@ -5,7 +5,6 @@ from eth_typing import Address, ChecksumAddress from src.providers.execution.base_interface import ContractInterface -from src.utils.types import hex_str_to_bytes HASH_REGREX = re.compile(r'^0x[0-9,A-F]{64}$', flags=re.IGNORECASE) ADDRESS_REGREX = re.compile('^0x[0-9,A-F]{40}$', flags=re.IGNORECASE) @@ -35,13 +34,9 @@ def check_contract( assert len(functions_spec) == len(log_with_call) -def check_value_re(regrex, value) -> None: - assert regrex.findall(value) - - def check_is_instance_of(type_: Type) -> Callable[[FuncArgs], None]: if type_ is Address or type_ is ChecksumAddress: - return check_is_address + return lambda resp: check_is_address(resp) and check_value_type(resp, type_) return lambda resp: check_value_type(resp, type_) @@ -51,4 +46,8 @@ def check_value_type(value, type_) -> None: def check_is_address(resp: FuncResp) -> None: assert isinstance(resp, str), "address should be returned as a string" - assert len(hex_str_to_bytes(resp)) == 20, "Got invalid address length" + check_value_re(ADDRESS_REGREX, resp) + + +def check_value_re(regrex, value) -> None: + assert regrex.findall(value), f"{value=} doesn't match {regrex=}" diff --git a/tests/integration/contracts/test_accounting_oracle.py b/tests/integration/contracts/test_accounting_oracle.py index d1a354f53..adb41541d 100644 --- a/tests/integration/contracts/test_accounting_oracle.py +++ b/tests/integration/contracts/test_accounting_oracle.py @@ -2,7 +2,7 @@ from web3.contract.contract import ContractFunction from src.modules.accounting.types import AccountingProcessingState -from tests.integration.contracts.contract_utils import check_contract, check_value_type +from tests.integration.contracts.contract_utils import check_contract, check_is_instance_of @pytest.mark.integration @@ -10,9 +10,9 @@ def test_accounting_oracle_contract(accounting_oracle_contract, caplog): check_contract( accounting_oracle_contract, [ - ('get_processing_state', None, lambda response: check_value_type(response, AccountingProcessingState)), - ('submit_report_extra_data_empty', None, lambda tx: check_value_type(tx, ContractFunction)), - ('submit_report_extra_data_list', (b'',), lambda tx: check_value_type(tx, ContractFunction)), + ('get_processing_state', None, check_is_instance_of(AccountingProcessingState)), + ('submit_report_extra_data_empty', None, check_is_instance_of(ContractFunction)), + ('submit_report_extra_data_list', (b'',), check_is_instance_of(ContractFunction)), ], caplog, ) diff --git a/tests/integration/contracts/test_bunker.py b/tests/integration/contracts/test_bunker.py index b8d4bef94..146810450 100644 --- a/tests/integration/contracts/test_bunker.py +++ b/tests/integration/contracts/test_bunker.py @@ -1,7 +1,7 @@ import pytest from src.modules.accounting.types import SharesRequestedToBurn -from tests.integration.contracts.contract_utils import check_contract, check_value_type +from tests.integration.contracts.contract_utils import check_contract, check_is_instance_of @pytest.mark.integration @@ -9,7 +9,7 @@ def test_burner(burner_contract, caplog): check_contract( burner_contract, [ - ('get_shares_requested_to_burn', None, lambda response: check_value_type(response, SharesRequestedToBurn)), + ('get_shares_requested_to_burn', None, check_is_instance_of(SharesRequestedToBurn)), ], caplog, ) diff --git a/tests/integration/contracts/test_cs_accounting.py b/tests/integration/contracts/test_cs_accounting.py index 46ae7f5be..60ae9ee65 100644 --- a/tests/integration/contracts/test_cs_accounting.py +++ b/tests/integration/contracts/test_cs_accounting.py @@ -1,5 +1,5 @@ import pytest -from eth_typing import Address +from eth_typing import ChecksumAddress from tests.integration.contracts.contract_utils import check_contract, check_is_instance_of @@ -9,7 +9,7 @@ def test_cs_accounting(cs_accounting_contract, caplog): check_contract( cs_accounting_contract, [ - ("fee_distributor", None, check_is_instance_of(Address)), + ("fee_distributor", None, check_is_instance_of(ChecksumAddress)), ("get_bond_curve_id", (0,), check_is_instance_of(int)), ], caplog, diff --git a/tests/integration/contracts/test_cs_fee_distributor.py b/tests/integration/contracts/test_cs_fee_distributor.py index 3821bd8e4..4f8c44fe6 100644 --- a/tests/integration/contracts/test_cs_fee_distributor.py +++ b/tests/integration/contracts/test_cs_fee_distributor.py @@ -1,5 +1,5 @@ import pytest -from eth_typing import Address +from eth_typing import ChecksumAddress from hexbytes import HexBytes from tests.integration.contracts.contract_utils import check_contract, check_is_instance_of @@ -10,7 +10,7 @@ def test_cs_fee_distributor(cs_fee_distributor_contract, caplog): check_contract( cs_fee_distributor_contract, [ - ("oracle", None, check_is_instance_of(Address)), + ("oracle", None, check_is_instance_of(ChecksumAddress)), ("shares_to_distribute", None, check_is_instance_of(int)), ("tree_root", None, check_is_instance_of(HexBytes)), ("tree_cid", None, check_is_instance_of(str)), diff --git a/tests/integration/contracts/test_cs_module.py b/tests/integration/contracts/test_cs_module.py index 37fab3a45..933956ce4 100644 --- a/tests/integration/contracts/test_cs_module.py +++ b/tests/integration/contracts/test_cs_module.py @@ -1,5 +1,5 @@ import pytest -from eth_typing import Address +from eth_typing import ChecksumAddress from tests.integration.contracts.contract_utils import check_contract, check_is_instance_of @@ -10,7 +10,7 @@ def test_cs_module(cs_module_contract, caplog): cs_module_contract, [ # ("parameters_registry", None, check_is_address), - ("accounting", None, check_is_instance_of(Address)), + ("accounting", None, check_is_instance_of(ChecksumAddress)), ("is_paused", None, check_is_instance_of(bool)), ], caplog, diff --git a/tests/integration/contracts/test_lido.py b/tests/integration/contracts/test_lido.py index f3a6115b5..aa6203ed9 100644 --- a/tests/integration/contracts/test_lido.py +++ b/tests/integration/contracts/test_lido.py @@ -1,17 +1,17 @@ import pytest from src.modules.accounting.types import LidoReportRebase, BeaconStat -from tests.integration.contracts.contract_utils import check_contract, check_value_type +from tests.integration.contracts.contract_utils import check_contract, check_is_instance_of @pytest.mark.integration -def test_lido_contract_call(lido_contract, accounting_oracle_contract, burner_contract, caplog): +def test_lido_contract_call(lido_contract, accounting_oracle_contract, caplog): check_contract( lido_contract, [ - ('get_buffered_ether', None, lambda response: check_value_type(response, int)), - ('total_supply', None, lambda response: check_value_type(response, int)), - ('get_beacon_stat', None, lambda response: check_value_type(response, BeaconStat)), + ('get_buffered_ether', None, check_is_instance_of(int)), + ('total_supply', None, check_is_instance_of(int)), + ('get_beacon_stat', None, check_is_instance_of(BeaconStat)), ( 'handle_oracle_report', ( @@ -27,7 +27,7 @@ def test_lido_contract_call(lido_contract, accounting_oracle_contract, burner_co # Call depends on contract state '0xfdc77ad0ea1ed99b1358beaca0d9c6fa831443f7f4c183302d9e2f76e4c9d0cb', ), - lambda response: check_value_type(response, LidoReportRebase), + check_is_instance_of(LidoReportRebase), ), ], caplog, diff --git a/tests/integration/contracts/test_lido_locator.py b/tests/integration/contracts/test_lido_locator.py index ef61a2728..bb6d00a22 100644 --- a/tests/integration/contracts/test_lido_locator.py +++ b/tests/integration/contracts/test_lido_locator.py @@ -1,7 +1,7 @@ import pytest from eth_typing import ChecksumAddress -from tests.integration.contracts.contract_utils import check_contract, check_value_type, check_value_re, ADDRESS_REGREX +from tests.integration.contracts.contract_utils import check_contract, check_is_instance_of @pytest.mark.integration @@ -9,66 +9,16 @@ def test_lido_locator_contract(lido_locator_contract, caplog): check_contract( lido_locator_contract, [ - ( - 'lido', - None, - lambda response: check_value_re(ADDRESS_REGREX, response) - and check_value_type(response, ChecksumAddress), - ), - ( - 'accounting_oracle', - None, - lambda response: check_value_re(ADDRESS_REGREX, response) - and check_value_type(response, ChecksumAddress), - ), - ( - 'staking_router', - None, - lambda response: check_value_re(ADDRESS_REGREX, response) - and check_value_type(response, ChecksumAddress), - ), - ( - 'validator_exit_bus_oracle', - None, - lambda response: check_value_re(ADDRESS_REGREX, response) - and check_value_type(response, ChecksumAddress), - ), - ( - 'withdrawal_queue', - None, - lambda response: check_value_re(ADDRESS_REGREX, response) - and check_value_type(response, ChecksumAddress), - ), - ( - 'oracle_report_sanity_checker', - None, - lambda response: check_value_re(ADDRESS_REGREX, response) - and check_value_type(response, ChecksumAddress), - ), - ( - 'oracle_daemon_config', - None, - lambda response: check_value_re(ADDRESS_REGREX, response) - and check_value_type(response, ChecksumAddress), - ), - ( - 'burner', - None, - lambda response: check_value_re(ADDRESS_REGREX, response) - and check_value_type(response, ChecksumAddress), - ), - ( - 'withdrawal_vault', - None, - lambda response: check_value_re(ADDRESS_REGREX, response) - and check_value_type(response, ChecksumAddress), - ), - ( - 'el_rewards_vault', - None, - lambda response: check_value_re(ADDRESS_REGREX, response) - and check_value_type(response, ChecksumAddress), - ), + ('lido', None, check_is_instance_of(ChecksumAddress)), + ('accounting_oracle', None, check_is_instance_of(ChecksumAddress)), + ('staking_router', None, check_is_instance_of(ChecksumAddress)), + ('validator_exit_bus_oracle', None, check_is_instance_of(ChecksumAddress)), + ('withdrawal_queue', None, check_is_instance_of(ChecksumAddress)), + ('oracle_report_sanity_checker', None, check_is_instance_of(ChecksumAddress)), + ('oracle_daemon_config', None, check_is_instance_of(ChecksumAddress)), + ('burner', None, check_is_instance_of(ChecksumAddress)), + ('withdrawal_vault', None, check_is_instance_of(ChecksumAddress)), + ('el_rewards_vault', None, check_is_instance_of(ChecksumAddress)), ], caplog, ) diff --git a/tests/integration/contracts/test_oracle_daemon_config.py b/tests/integration/contracts/test_oracle_daemon_config.py index 34487602c..5383860b8 100644 --- a/tests/integration/contracts/test_oracle_daemon_config.py +++ b/tests/integration/contracts/test_oracle_daemon_config.py @@ -1,7 +1,7 @@ import pytest from src.constants import TOTAL_BASIS_POINTS -from tests.integration.contracts.contract_utils import check_contract, check_value_type +from tests.integration.contracts.contract_utils import check_contract, check_is_instance_of, check_value_type @pytest.mark.integration @@ -9,47 +9,23 @@ def test_oracle_daemon_config_contract(oracle_daemon_config_contract, caplog): check_contract( oracle_daemon_config_contract, [ - ('normalized_cl_reward_per_epoch', None, lambda response: check_value_type(response, int)), ( 'normalized_cl_reward_mistake_rate_bp', None, lambda response: check_value_type(response, int) and response < TOTAL_BASIS_POINTS, ), - ( - 'rebase_check_nearest_epoch_distance', - None, - lambda response: check_value_type(response, int), - ), - ( - 'rebase_check_distant_epoch_distance', - None, - lambda response: check_value_type(response, int), - ), ( 'node_operator_network_penetration_threshold_bp', None, lambda response: check_value_type(response, int) and response < TOTAL_BASIS_POINTS, ), - ( - 'prediction_duration_in_slots', - None, - lambda response: check_value_type(response, int), - ), - ( - 'finalization_max_negative_rebase_epoch_shift', - None, - lambda response: check_value_type(response, int), - ), - ( - 'validator_delayed_timeout_in_slots', - None, - lambda response: check_value_type(response, int), - ), - ( - 'validator_delinquent_timeout_in_slots', - None, - lambda response: check_value_type(response, int), - ), + ('normalized_cl_reward_per_epoch', None, check_is_instance_of(int)), + ('rebase_check_nearest_epoch_distance', None, check_is_instance_of(int)), + ('rebase_check_distant_epoch_distance', None, check_is_instance_of(int)), + ('prediction_duration_in_slots', None, check_is_instance_of(int)), + ('finalization_max_negative_rebase_epoch_shift', None, check_is_instance_of(int)), + ('validator_delayed_timeout_in_slots', None, check_is_instance_of(int)), + ('validator_delinquent_timeout_in_slots', None, check_is_instance_of(int)), ], caplog, ) diff --git a/tests/integration/contracts/test_oracle_report_sanity_checker.py b/tests/integration/contracts/test_oracle_report_sanity_checker.py index 0b69b3fde..aa99c5343 100644 --- a/tests/integration/contracts/test_oracle_report_sanity_checker.py +++ b/tests/integration/contracts/test_oracle_report_sanity_checker.py @@ -1,7 +1,7 @@ import pytest from src.modules.accounting.types import OracleReportLimits -from tests.integration.contracts.contract_utils import check_contract, check_value_type +from tests.integration.contracts.contract_utils import check_contract, check_is_instance_of @pytest.mark.integration @@ -9,7 +9,7 @@ def test_oracle_report_sanity_checker(oracle_report_sanity_checker_contract, cap check_contract( oracle_report_sanity_checker_contract, [ - ('get_oracle_report_limits', None, lambda response: check_value_type(response, OracleReportLimits)), + ('get_oracle_report_limits', None, check_is_instance_of(OracleReportLimits)), ], caplog, ) diff --git a/tests/integration/contracts/test_validator_exit_bus_oracle.py b/tests/integration/contracts/test_validator_exit_bus_oracle.py index d89bad8e1..7c3833b1d 100644 --- a/tests/integration/contracts/test_validator_exit_bus_oracle.py +++ b/tests/integration/contracts/test_validator_exit_bus_oracle.py @@ -1,7 +1,7 @@ import pytest from src.modules.ejector.types import EjectorProcessingState -from tests.integration.contracts.contract_utils import check_contract, check_value_type +from tests.integration.contracts.contract_utils import check_contract, check_is_instance_of, check_value_type @pytest.mark.integration @@ -9,12 +9,15 @@ def test_vebo(validators_exit_bus_oracle_contract, caplog): check_contract( validators_exit_bus_oracle_contract, [ - ('is_paused', None, lambda response: check_value_type(response, bool)), - ('get_processing_state', None, lambda response: check_value_type(response, EjectorProcessingState)), + ('is_paused', None, check_is_instance_of(bool)), + ('get_processing_state', None, check_is_instance_of(EjectorProcessingState)), ( 'get_last_requested_validator_indices', (1, [1]), - lambda response: check_value_type(response, list) and map(lambda val: check_value_type(val, int)), + lambda response: check_value_type(response, list) and map(lambda val: + check_value_type(val, + int), + response), ), ], caplog, diff --git a/tests/integration/contracts/test_withdrawal_queue_nft_contract.py b/tests/integration/contracts/test_withdrawal_queue_nft_contract.py index 6bcf2807e..c94712487 100644 --- a/tests/integration/contracts/test_withdrawal_queue_nft_contract.py +++ b/tests/integration/contracts/test_withdrawal_queue_nft_contract.py @@ -1,7 +1,7 @@ import pytest from src.modules.accounting.types import BatchState, WithdrawalRequestStatus -from tests.integration.contracts.contract_utils import check_contract, check_value_type +from tests.integration.contracts.contract_utils import check_contract, check_is_instance_of @pytest.mark.integration @@ -9,13 +9,13 @@ def test_withdrawal_queue(withdrawal_queue_nft_contract, caplog): check_contract( withdrawal_queue_nft_contract, [ - ('unfinalized_steth', None, lambda response: check_value_type(response, int)), - ('bunker_mode_since_timestamp', None, lambda response: check_value_type(response, int)), - ('get_last_finalized_request_id', None, lambda response: check_value_type(response, int)), - ('get_withdrawal_status', (1,), lambda response: check_value_type(response, WithdrawalRequestStatus)), - ('get_last_request_id', None, lambda response: check_value_type(response, int)), - ('is_paused', None, lambda response: check_value_type(response, bool)), - ('max_batches_length', None, lambda response: check_value_type(response, int)), + ('unfinalized_steth', None, check_is_instance_of(int)), + ('bunker_mode_since_timestamp', None, check_is_instance_of(int)), + ('get_last_finalized_request_id', None, check_is_instance_of(int)), + ('get_withdrawal_status', (1,), check_is_instance_of(WithdrawalRequestStatus)), + ('get_last_request_id', None, check_is_instance_of(int)), + ('is_paused', None, check_is_instance_of(bool)), + ('max_batches_length', None, check_is_instance_of(int)), ( 'calculate_finalization_batches', ( @@ -67,7 +67,7 @@ def test_withdrawal_queue(withdrawal_queue_nft_contract, caplog): ), "0xcc74e3f3fe27f9f8da8b91c1aede727ebfa82d9c74cf4b873d81269e866dce72", ), - lambda response: check_value_type(response, BatchState), + check_is_instance_of(BatchState), ), ], caplog, From a2e806d60d48d3ca318e1d7047701f507701e7ff Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Thu, 6 Mar 2025 16:14:02 +0100 Subject: [PATCH 098/162] chore: black --- .../integration/contracts/test_validator_exit_bus_oracle.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/integration/contracts/test_validator_exit_bus_oracle.py b/tests/integration/contracts/test_validator_exit_bus_oracle.py index 7c3833b1d..49675451e 100644 --- a/tests/integration/contracts/test_validator_exit_bus_oracle.py +++ b/tests/integration/contracts/test_validator_exit_bus_oracle.py @@ -14,10 +14,8 @@ def test_vebo(validators_exit_bus_oracle_contract, caplog): ( 'get_last_requested_validator_indices', (1, [1]), - lambda response: check_value_type(response, list) and map(lambda val: - check_value_type(val, - int), - response), + lambda response: check_value_type(response, list) + and map(lambda val: check_value_type(val, int), response), ), ], caplog, From 0aa11aff7bd46857300e8763f10ed8b4074d6991 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Thu, 6 Mar 2025 16:19:08 +0100 Subject: [PATCH 099/162] feat: no rebate if no distribution --- src/modules/csm/distribution.py | 4 ++-- tests/modules/csm/test_csm_distribution.py | 26 +++++++++++----------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/modules/csm/distribution.py b/src/modules/csm/distribution.py index 1f3e241a2..416359631 100644 --- a/src/modules/csm/distribution.py +++ b/src/modules/csm/distribution.py @@ -166,9 +166,9 @@ def _calculate_distribution_in_frame( participation_shares, total_rebate_share, rewards_to_distribute ) distributed_rewards = sum(rewards_distribution_map.values()) - # All rewards to distribute can be rebated if no duties were assigned to validators or + # All rewards to distribute should not be rebated if no duties were assigned to validators or # all validators were below threshold. - rebate_to_protocol = rewards_to_distribute - distributed_rewards + rebate_to_protocol = 0 if not distributed_rewards else rewards_to_distribute - distributed_rewards for no_id, no_rewards in rewards_distribution_map.items(): log.operators[no_id].distributed = no_rewards diff --git a/tests/modules/csm/test_csm_distribution.py b/tests/modules/csm/test_csm_distribution.py index dc4483585..97857c165 100644 --- a/tests/modules/csm/test_csm_distribution.py +++ b/tests/modules/csm/test_csm_distribution.py @@ -196,14 +196,14 @@ # distributed_rewards 0, # rebate_to_protocol - 500, + 0, # strikes {(NodeOperatorId(1), HexBytes("0x01")): 1}, ) ], 0, {NodeOperatorId(1): 500}, - 500, + 0, {(NodeOperatorId(1), HexBytes("0x01")): [1, 1, 0, 0, 0, 1]}, ), # Multiple frames, some of which are not distributed @@ -241,15 +241,15 @@ # distributed_rewards 0, # rebate_to_protocol - 500, + 0, # strikes {(NodeOperatorId(1), HexBytes("0x01")): 1}, ), ( # rewards - {NodeOperatorId(1): 700}, + {NodeOperatorId(1): 500 + 700}, # distributed_rewards - 700, + 500 + 700, # rebate_to_protocol 0, # strikes @@ -261,14 +261,14 @@ # distributed_rewards 0, # rebate_to_protocol - 300, + 0, # strikes {}, ), ], - 700, - {NodeOperatorId(1): 500 + 700}, - 800, + 500 + 700, + {NodeOperatorId(1): 500 + 500 + 700}, + 0, { (NodeOperatorId(1), HexBytes("0x01")): [0, 0, 1, 1, 0, 0], (NodeOperatorId(2), HexBytes("0x02")): [0, 1, 0, 0, 0, 0], @@ -486,7 +486,7 @@ def test_calculate_distribution_handles_invalid_distribution_in_total(): # Distributed rewards 0, # Rebate to protocol - 100, + 0, # Strikes { (NodeOperatorId(1), HexBytes('0x01')): 1, @@ -496,7 +496,7 @@ def test_calculate_distribution_handles_invalid_distribution_in_total(): frame=..., distributable=100, distributed_rewards=0, - rebate_to_protocol=100, + rebate_to_protocol=0, operators={ NodeOperatorId(1): OperatorFrameSummary( distributed=0, @@ -757,7 +757,7 @@ def test_calculate_distribution_handles_invalid_distribution_in_total(): # Distributed rewards 0, # Rebate to protocol - 100, + 0, # Strikes {}, FramePerfLog( @@ -765,7 +765,7 @@ def test_calculate_distribution_handles_invalid_distribution_in_total(): frame=..., distributable=100, distributed_rewards=0, - rebate_to_protocol=100, + rebate_to_protocol=0, operators={}, ), ), From 296183a625f8a944399d0e59cb750df9193f48ad Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Thu, 6 Mar 2025 16:52:55 +0100 Subject: [PATCH 100/162] chore: add assert to make sure a contract is instantiated --- tests/integration/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 836e3a7f2..9870f4245 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -34,6 +34,7 @@ def web3_provider_integration(request): def get_contract(w3, contract_class, address): + assert address, "No address given" return cast( contract_class, w3.eth.contract( From 0e75bec9fed48f11425f0c8812541e7fa471d7c5 Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Thu, 6 Mar 2025 16:54:27 +0100 Subject: [PATCH 101/162] fix: add CSM address to test --- tests/integration/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 9870f4245..5e3b94700 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -136,7 +136,7 @@ def cs_module_contract(web3_provider_integration): return get_contract( web3_provider_integration, CSModuleContract, - variables.CSM_MODULE_ADDRESS, + "0xdA7dE2ECdDfccC6c3AF10108Db212ACBBf9EA83F", # mainnet deploy ) From 97201a06c52e1331a2bd7374d0bec6cda06d2df0 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Fri, 7 Mar 2025 11:47:18 +0100 Subject: [PATCH 102/162] fix: review --- src/modules/csm/distribution.py | 8 +--- tests/modules/csm/test_csm_distribution.py | 47 +++++++++++++--------- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/src/modules/csm/distribution.py b/src/modules/csm/distribution.py index 416359631..c29913fef 100644 --- a/src/modules/csm/distribution.py +++ b/src/modules/csm/distribution.py @@ -278,8 +278,7 @@ def _merge_strikes( acc[key] = StrikesList() acc[key].push(strikes_in_frame[key]) - keys_to_delete = [] - for key in acc: + for key in list(acc.keys()): no_id, _ = key if key not in strikes_in_frame: acc[key].push(StrikesList.SENTINEL) # Just shifting... @@ -287,7 +286,4 @@ def _merge_strikes( acc[key].resize(maxlen) # NOTE: Cleanup sequences like [0,0,0] since they don't bring any information. if not sum(acc[key]): - keys_to_delete.append(key) - - for key in keys_to_delete: - del acc[key] + del acc[key] diff --git a/tests/modules/csm/test_csm_distribution.py b/tests/modules/csm/test_csm_distribution.py index 97857c165..24fb9cbb6 100644 --- a/tests/modules/csm/test_csm_distribution.py +++ b/tests/modules/csm/test_csm_distribution.py @@ -28,16 +28,18 @@ @pytest.mark.parametrize( - "frames, " - "last_report, " - "mocked_curve_params, " - "frame_blockstamps, " - "shares_to_distribute, " - "distribution_in_frame, " - "expected_total_rewards, " - "expected_total_rewards_map, " - "expected_total_rebate," - "expected_strikes", + ( + "frames", + "last_report", + "mocked_curve_params", + "frame_blockstamps", + "shares_to_distribute", + "distribution_in_frame", + "expected_total_rewards", + "expected_total_rewards_map", + "expected_total_rebate", + "expected_strikes", + ), [ # One frame ( @@ -367,15 +369,17 @@ def test_calculate_distribution_handles_invalid_distribution_in_total(): @pytest.mark.parametrize( - "to_distribute, " - "frame_validators, " - "frame_state_data, " - "mocked_curve_params, " - "expected_rewards_distribution_map, " - "expected_distributed_rewards, " - "expected_rebate_to_protocol, " - "expected_frame_strikes, " - "expected_log", + ( + "to_distribute", + "frame_validators", + "frame_state_data", + "mocked_curve_params", + "expected_rewards_distribution_map", + "expected_distributed_rewards", + "expected_rebate_to_protocol", + "expected_frame_strikes", + "expected_log", + ), [ # All above threshold performance ( @@ -1073,7 +1077,6 @@ def tests_validates_correct_distribution(total_distributed_rewards, total_rebate "total_distributed_rewards, total_rebate, total_rewards_to_distribute", [ (100, 51, 150), - (100, 50, 149), (200, 0, 199), ], ) @@ -1109,7 +1112,9 @@ def test_performance_coefficients_calc_performance(attestation_perf, proposal_pe ([1], [10000, 9000], 1, 10000 / TOTAL_BASIS_POINTS), ([1], [10000, 9000], 2, 9000 / TOTAL_BASIS_POINTS), ([10, 20], [1000, 2000, 3000], 5, 1000 / TOTAL_BASIS_POINTS), + ([10, 20], [1000, 2000, 3000], 10, 1000 / TOTAL_BASIS_POINTS), ([10, 20], [1000, 2000, 3000], 15, 2000 / TOTAL_BASIS_POINTS), + ([10, 20], [1000, 2000, 3000], 20, 2000 / TOTAL_BASIS_POINTS), ([10, 20], [1000, 2000, 3000], 25, 3000 / TOTAL_BASIS_POINTS), ], ) @@ -1137,7 +1142,9 @@ def test_performance_leeway_raises_error_for_key_number_out_of_range(): ([1], [10000, 9000], 1, 10000 / TOTAL_BASIS_POINTS), ([1], [10000, 9000], 2, 9000 / TOTAL_BASIS_POINTS), ([10, 20], [1000, 2000, 3000], 5, 1000 / TOTAL_BASIS_POINTS), + ([10, 20], [1000, 2000, 3000], 10, 1000 / TOTAL_BASIS_POINTS), ([10, 20], [1000, 2000, 3000], 15, 2000 / TOTAL_BASIS_POINTS), + ([10, 20], [1000, 2000, 3000], 20, 2000 / TOTAL_BASIS_POINTS), ([10, 20], [1000, 2000, 3000], 25, 3000 / TOTAL_BASIS_POINTS), ], ) From f78683922d3dbd0d922239dd7b40b4e561c206ea Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Fri, 7 Mar 2025 13:12:34 +0100 Subject: [PATCH 103/162] chore: add xfail tests for CSM v2 --- tests/integration/contracts/test_cs_fee_oracle.py | 15 ++++++++++++++- tests/integration/contracts/test_cs_module.py | 14 +++++++++++++- .../contracts/test_cs_parameters_registry.py | 3 ++- tests/integration/contracts/test_cs_strikes.py | 7 ++++--- 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/tests/integration/contracts/test_cs_fee_oracle.py b/tests/integration/contracts/test_cs_fee_oracle.py index 0504ac8b3..659bd283e 100644 --- a/tests/integration/contracts/test_cs_fee_oracle.py +++ b/tests/integration/contracts/test_cs_fee_oracle.py @@ -1,4 +1,6 @@ import pytest +from eth_typing import ChecksumAddress +from web3.exceptions import ContractLogicError from tests.integration.contracts.contract_utils import check_contract, check_is_instance_of @@ -8,8 +10,19 @@ def test_cs_fee_oracle(cs_fee_oracle_contract, caplog): check_contract( cs_fee_oracle_contract, [ - # ("strikes", None, check_is_instance_of(Address)), FIXME: Uncomment with CSMv2 live on mainnet. ("is_paused", None, check_is_instance_of(bool)), ], caplog, ) + + +@pytest.mark.integration +@pytest.mark.xfail(raises=ContractLogicError, reason="CSMv2 is not yet live") +def test_cs_fee_oracle_v2(cs_fee_oracle_contract, caplog): + check_contract( + cs_fee_oracle_contract, + [ + ("strikes", None, check_is_instance_of(ChecksumAddress)), + ], + caplog, + ) diff --git a/tests/integration/contracts/test_cs_module.py b/tests/integration/contracts/test_cs_module.py index 933956ce4..7b726d6ee 100644 --- a/tests/integration/contracts/test_cs_module.py +++ b/tests/integration/contracts/test_cs_module.py @@ -1,5 +1,6 @@ import pytest from eth_typing import ChecksumAddress +from web3.exceptions import ContractLogicError from tests.integration.contracts.contract_utils import check_contract, check_is_instance_of @@ -9,9 +10,20 @@ def test_cs_module(cs_module_contract, caplog): check_contract( cs_module_contract, [ - # ("parameters_registry", None, check_is_address), ("accounting", None, check_is_instance_of(ChecksumAddress)), ("is_paused", None, check_is_instance_of(bool)), ], caplog, ) + + +@pytest.mark.integration +@pytest.mark.xfail(raises=ContractLogicError, reason="CSMv2 is not yet live") +def test_cs_module_v2(cs_module_contract, caplog): + check_contract( + cs_module_contract, + [ + ("parameters_registry", None, check_is_instance_of(ChecksumAddress)), + ], + caplog, + ) diff --git a/tests/integration/contracts/test_cs_parameters_registry.py b/tests/integration/contracts/test_cs_parameters_registry.py index 39e4d5e34..917b04abc 100644 --- a/tests/integration/contracts/test_cs_parameters_registry.py +++ b/tests/integration/contracts/test_cs_parameters_registry.py @@ -1,4 +1,5 @@ import pytest +from web3.exceptions import ContractLogicError from src.providers.execution.contracts.cs_parameters_registry import ( PerformanceCoefficients, @@ -10,7 +11,7 @@ @pytest.mark.integration -@pytest.mark.skip("Requires CSMv2 activated on mainnet") # TODO: Remove the mark with CSM v2 live on mainnet +@pytest.mark.xfail(raises=ContractLogicError, reason="CSMv2 is not yet live") def test_cs_parameters_registry(cs_params_contract, caplog): check_contract( cs_params_contract, diff --git a/tests/integration/contracts/test_cs_strikes.py b/tests/integration/contracts/test_cs_strikes.py index bb289082f..360bc4bf6 100644 --- a/tests/integration/contracts/test_cs_strikes.py +++ b/tests/integration/contracts/test_cs_strikes.py @@ -1,14 +1,15 @@ import pytest from hexbytes import HexBytes +from web3.exceptions import ContractLogicError from tests.integration.contracts.contract_utils import check_contract, check_is_instance_of @pytest.mark.integration -@pytest.mark.skip("Requires CSMv2 activated on mainnet") # TODO: Remove the mark with CSM v2 live on mainnet -def test_cs_strikes(cs_module_contract, caplog): +@pytest.mark.xfail(raises=ContractLogicError, reason="CSMv2 is not yet live") +def test_cs_strikes(cs_strikes_contract, caplog): check_contract( - cs_module_contract, + cs_strikes_contract, [ ("tree_root", None, check_is_instance_of(HexBytes)), ("tree_cid", None, check_is_instance_of(str)), From 58fb97f98b3c32123f79cd6e3de3d433ebdaa0eb Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Wed, 19 Mar 2025 13:10:42 +0100 Subject: [PATCH 104/162] fix: clear frames in state.clear --- src/modules/csm/state.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/csm/state.py b/src/modules/csm/state.py index 4bebb658f..581197e5e 100644 --- a/src/modules/csm/state.py +++ b/src/modules/csm/state.py @@ -149,6 +149,7 @@ def _calculate_frames(epochs_to_process: tuple[EpochNumber, ...], epochs_per_fra return [(frame[0], frame[-1]) for frame in batched(sorted(epochs_to_process), epochs_per_frame)] def clear(self) -> None: + self.frames = [] self.data = {} self._epochs_to_process = tuple() self._processed_epochs.clear() From 9245e0dd176f8df75219ca6e7d4df71fa2893f80 Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Fri, 21 Mar 2025 13:12:14 +0100 Subject: [PATCH 105/162] fix: missing perf coefficients logging --- src/modules/csm/distribution.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/csm/distribution.py b/src/modules/csm/distribution.py index c29913fef..4c396bd4e 100644 --- a/src/modules/csm/distribution.py +++ b/src/modules/csm/distribution.py @@ -139,6 +139,7 @@ def _calculate_distribution_in_frame( log_operator = log.operators[no_id] curve_params = self.w3.csm.get_curve_params(no_id, blockstamp) + log_operator.performance_coefficients = curve_params.perf_coeffs sorted_active_validators = sorted(active_validators, key=lambda v: v.index) numbered_validators = enumerate(sorted_active_validators, start=1) From 8015c0d22c74a747c2a16c1b4141dd9b725aa55f Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Fri, 21 Mar 2025 15:23:28 +0100 Subject: [PATCH 106/162] test: refactor state tests to use random state file location --- tests/modules/csm/test_state.py | 76 ++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 35 deletions(-) diff --git a/tests/modules/csm/test_state.py b/tests/modules/csm/test_state.py index 703bd9d00..1d32f6f9d 100644 --- a/tests/modules/csm/test_state.py +++ b/tests/modules/csm/test_state.py @@ -7,24 +7,41 @@ import pytest from src import variables -from src.modules.csm.state import State, InvalidState, DutyAccumulator, NetworkDuties +from src.modules.csm.state import DutyAccumulator, InvalidState, NetworkDuties, State from src.types import ValidatorIndex from src.utils.range import sequence +@pytest.fixture() +def state_file_path(tmp_path: Path) -> Path: + return (tmp_path / "mock").with_suffix(State.EXTENSION) + + @pytest.fixture(autouse=True) -def remove_state_files(): - state_file = Path("/tmp/state.pkl") - state_buf = Path("/tmp/state.buf") - state_file.unlink(missing_ok=True) - state_buf.unlink(missing_ok=True) - yield - state_file.unlink(missing_ok=True) - state_buf.unlink(missing_ok=True) +def mock_state_file(state_file_path: Path): + State.file = Mock(return_value=state_file_path) + + +class TestCachePathConfigurable: + @pytest.fixture() + def mock_state_file(self): + # NOTE: Overrides file-level mock_state_file to check the mechanic. + pass + + @pytest.fixture() + def cache_path(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: + monkeypatch.setattr(variables, "CACHE_PATH", tmp_path) + return tmp_path + + def test_file_returns_correct_path(self, cache_path: Path): + assert State.file() == cache_path / "cache.pkl" + + def test_buffer_returns_correct_path(self, cache_path: Path): + state = State() + assert state.buffer == cache_path / "cache.buf" -def test_load_restores_state_from_file(monkeypatch): - monkeypatch.setattr("src.modules.csm.state.State.file", lambda _=None: Path("/tmp/state.pkl")) +def test_load_restores_state_from_file(): state = State() state.data = { (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), @@ -34,43 +51,32 @@ def test_load_restores_state_from_file(monkeypatch): assert loaded_state.data == state.data -def test_load_returns_new_instance_if_file_not_found(monkeypatch): - monkeypatch.setattr("src.modules.csm.state.State.file", lambda: Path("/non/existent/path")) +def test_load_returns_new_instance_if_file_not_found(state_file_path: Path): + assert not state_file_path.exists() state = State.load() assert state.is_empty -def test_load_returns_new_instance_if_empty_object(monkeypatch, tmp_path): - with open('/tmp/state.pkl', "wb") as f: +def test_load_returns_new_instance_if_empty_object(state_file_path: Path): + with open(state_file_path, "wb") as f: pickle.dump(None, f) - monkeypatch.setattr("src.modules.csm.state.State.file", lambda: Path("/tmp/state.pkl")) state = State.load() assert state.is_empty -def test_commit_saves_state_to_file(monkeypatch): +def test_commit_saves_state_to_file(state_file_path: Path, monkeypatch: pytest.MonkeyPatch): state = State() state.data = { (0, 31): defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), } - monkeypatch.setattr("src.modules.csm.state.State.file", lambda _: Path("/tmp/state.pkl")) - monkeypatch.setattr("os.replace", Mock(side_effect=os.replace)) - state.commit() - with open("/tmp/state.pkl", "rb") as f: - loaded_state = pickle.load(f) - assert loaded_state.data == state.data - os.replace.assert_called_once_with(Path("/tmp/state.buf"), Path("/tmp/state.pkl")) - - -def test_file_returns_correct_path(monkeypatch): - monkeypatch.setattr(variables, "CACHE_PATH", Path("/tmp")) - assert State.file() == Path("/tmp/cache.pkl") - - -def test_buffer_returns_correct_path(monkeypatch): - monkeypatch.setattr(variables, "CACHE_PATH", Path("/tmp")) - state = State() - assert state.buffer == Path("/tmp/cache.buf") + with monkeypatch.context() as mp: + os_replace_mock = Mock(side_effect=os.replace) + mp.setattr("os.replace", os_replace_mock) + state.commit() + with open(state_file_path, "rb") as f: + loaded_state = pickle.load(f) + assert loaded_state.data == state.data + os_replace_mock.assert_called_once_with(state_file_path.with_suffix(".buf"), state_file_path) def test_is_empty_returns_true_for_empty_state(): From 06dd20d7dd5dff146408c89433b12416636b5e54 Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Fri, 21 Mar 2025 15:37:32 +0100 Subject: [PATCH 107/162] test: fix test failed because of logging perf coeffs --- tests/modules/csm/test_csm_distribution.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/modules/csm/test_csm_distribution.py b/tests/modules/csm/test_csm_distribution.py index 24fb9cbb6..677c3b33f 100644 --- a/tests/modules/csm/test_csm_distribution.py +++ b/tests/modules/csm/test_csm_distribution.py @@ -431,7 +431,7 @@ def test_calculate_distribution_handles_invalid_distribution_in_total(): ValidatorIndex(1): ValidatorFrameSummary( performance=0.6, threshold=0.5, - rewards_share=1, + rewards_share=1.0, slashed=False, strikes=0, attestation_duty=DutyAccumulator(assigned=10, included=6), @@ -509,7 +509,7 @@ def test_calculate_distribution_handles_invalid_distribution_in_total(): ValidatorIndex(1): ValidatorFrameSummary( performance=0.5, threshold=0.65, - rewards_share=1, + rewards_share=1.0, slashed=False, strikes=1, attestation_duty=DutyAccumulator(assigned=10, included=5), @@ -707,14 +707,18 @@ def test_calculate_distribution_handles_invalid_distribution_in_total(): ), NodeOperatorId(5): OperatorFrameSummary( distributed=47, - performance_coefficients=PerformanceCoefficients(), + performance_coefficients=PerformanceCoefficients( + attestations_weight=1, + blocks_weight=0, + sync_weight=0, + ), validators=defaultdict( ValidatorFrameSummary, { ValidatorIndex(7): ValidatorFrameSummary( performance=1.0, threshold=0.8842289719626168, - rewards_share=1, + rewards_share=1.0, slashed=False, strikes=0, attestation_duty=DutyAccumulator(assigned=10, included=10), From 657adf81ad51f7e4d18b56887ac75b7538a99477 Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Tue, 25 Mar 2025 10:47:17 +0100 Subject: [PATCH 108/162] chore: missing word in log --- src/modules/csm/helpers/last_report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/csm/helpers/last_report.py b/src/modules/csm/helpers/last_report.py index 7c73d1ed1..9752ffd58 100644 --- a/src/modules/csm/helpers/last_report.py +++ b/src/modules/csm/helpers/last_report.py @@ -83,7 +83,7 @@ def strikes(self) -> dict[StrikesValidator, StrikesList]: logger.info({"msg": f"No strikes reported as of {self.blockstamp=}."}) return {} - logger.info({"msg": "Fetching tree by CID from IPFS", "cid": repr(self.strikes_tree_cid)}) + logger.info({"msg": "Fetching strikes tree by CID from IPFS", "cid": repr(self.strikes_tree_cid)}) tree = StrikesTree.decode(self.w3.ipfs.fetch(self.strikes_tree_cid)) logger.info({"msg": "Restored strikes tree from IPFS dump", "root": repr(tree.root)}) From b171789aa494bfc3832c6393c15057f70e2afb6f Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Fri, 28 Mar 2025 11:15:05 +0100 Subject: [PATCH 109/162] tests: fix `deployer` --- tests/fork/test_csm_oracle_cycle.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/fork/test_csm_oracle_cycle.py b/tests/fork/test_csm_oracle_cycle.py index d7cc7e2a3..7ca79eb6f 100644 --- a/tests/fork/test_csm_oracle_cycle.py +++ b/tests/fork/test_csm_oracle_cycle.py @@ -63,11 +63,10 @@ def update_csm_to_v2(accounts_from_fork, forked_el_client: Web3, anvil_port: int os.chdir(prepared_csm_repo) with subprocess.Popen( - ['just', '_deploy-impl', '--broadcast'], + ['just', '_deploy-impl', '--broadcast', '--private-key', deployer], env={ **os.environ, "ANVIL_PORT": str(anvil_port), - 'DEPLOYER_PRIVATE_KEY': deployer, 'CHAIN': chain, }, stdout=subprocess.DEVNULL, From cf30ad37d103f41b6370659dbe317970ae4317c6 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Fri, 28 Mar 2025 14:39:08 +0100 Subject: [PATCH 110/162] feat: reset main cycle timeout --- src/modules/csm/csm.py | 3 ++- src/modules/submodules/oracle_module.py | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index 62d34ee1b..63b67f75b 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -210,7 +210,8 @@ def collect_data(self, blockstamp: BlockStamp) -> bool: logger.info({"msg": "Checkpoints were prepared for an outdated epochs range, stop processing"}) raise ValueError("Outdated checkpoint") processor.exec(checkpoint) - + # Reset BaseOracle cycle timeout to avoid timeout errors during long checkpoints processing + self._reset_cycle_timeout() return self.state.is_fulfilled def make_rewards_tree(self, shares: dict[NodeOperatorId, Shares]) -> RewardsTree: diff --git a/src/modules/submodules/oracle_module.py b/src/modules/submodules/oracle_module.py index d2bd42241..ad284da2a 100644 --- a/src/modules/submodules/oracle_module.py +++ b/src/modules/submodules/oracle_module.py @@ -1,4 +1,5 @@ import logging +import signal import time import traceback from abc import abstractmethod, ABC @@ -112,6 +113,12 @@ def _cycle(self): except ValueError as error: logger.error({'msg': 'Unexpected error.', 'error': str(error)}) + @staticmethod + def _reset_cycle_timeout(): + """Reset the timeout timer for the current cycle.""" + logger.info({'msg': f'Reset running cycle timeout to {variables.MAX_CYCLE_LIFETIME_IN_SECONDS} seconds'}) + signal.setitimer(signal.ITIMER_REAL, variables.MAX_CYCLE_LIFETIME_IN_SECONDS) + @staticmethod def _sleep_cycle(): """Handles sleeping between cycles based on the configured cycle sleep time.""" From 2654954e4d38b3eb33ad741f58b36225926ab69d Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Fri, 25 Apr 2025 17:17:14 +0200 Subject: [PATCH 111/162] fix: cs params --- assets/CSParametersRegistry.json | 2 +- src/modules/csm/distribution.py | 8 +- .../contracts/cs_parameters_registry.py | 52 ++++----- .../contracts/test_cs_parameters_registry.py | 7 +- tests/modules/csm/test_csm_distribution.py | 105 +++++++++--------- 5 files changed, 78 insertions(+), 96 deletions(-) diff --git a/assets/CSParametersRegistry.json b/assets/CSParametersRegistry.json index 7db794b4f..0abea6d1e 100644 --- a/assets/CSParametersRegistry.json +++ b/assets/CSParametersRegistry.json @@ -1 +1 @@ -[{"type":"constructor","inputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"defaultBadPerformancePenalty","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultElRewardsStealingAdditionalFine","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultKeyRemovalCharge","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultPerformanceCoefficients","inputs":[],"outputs":[{"name":"attestationsWeight","type":"uint32","internalType":"uint32"},{"name":"blocksWeight","type":"uint32","internalType":"uint32"},{"name":"syncWeight","type":"uint32","internalType":"uint32"}],"stateMutability":"view"},{"type":"function","name":"defaultPerformanceLeeway","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultPriorityQueueLimit","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultRewardShare","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultStrikesParams","inputs":[],"outputs":[{"name":"lifetime","type":"uint32","internalType":"uint32"},{"name":"threshold","type":"uint32","internalType":"uint32"}],"stateMutability":"view"},{"type":"function","name":"getBadPerformancePenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"penalty","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getElRewardsStealingAdditionalFine","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"fine","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getKeyRemovalCharge","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"keyRemovalCharge","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getPerformanceCoefficients","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"attestationsWeight","type":"uint256","internalType":"uint256"},{"name":"blocksWeight","type":"uint256","internalType":"uint256"},{"name":"syncWeight","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getPerformanceLeewayData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"keyPivots","type":"uint256[]","internalType":"uint256[]"},{"name":"performanceLeeways","type":"uint256[]","internalType":"uint256[]"}],"stateMutability":"view"},{"type":"function","name":"getPriorityQueueLimit","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"limit","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRewardShareData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"keyPivots","type":"uint256[]","internalType":"uint256[]"},{"name":"rewardShares","type":"uint256[]","internalType":"uint256[]"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getStrikesParams","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"lifetime","type":"uint256","internalType":"uint256"},{"name":"threshold","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"admin","type":"address","internalType":"address"},{"name":"data","type":"tuple","internalType":"struct ICSParametersRegistry.InitializationData","components":[{"name":"keyRemovalCharge","type":"uint256","internalType":"uint256"},{"name":"elRewardsStealingAdditionalFine","type":"uint256","internalType":"uint256"},{"name":"priorityQueueLimit","type":"uint256","internalType":"uint256"},{"name":"rewardShare","type":"uint256","internalType":"uint256"},{"name":"performanceLeeway","type":"uint256","internalType":"uint256"},{"name":"strikesLifetime","type":"uint256","internalType":"uint256"},{"name":"strikesThreshold","type":"uint256","internalType":"uint256"},{"name":"badPerformancePenalty","type":"uint256","internalType":"uint256"},{"name":"attestationsWeight","type":"uint256","internalType":"uint256"},{"name":"blocksWeight","type":"uint256","internalType":"uint256"},{"name":"syncWeight","type":"uint256","internalType":"uint256"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setBadPerformancePenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"penalty","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultBadPerformancePenalty","inputs":[{"name":"penalty","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultElRewardsStealingAdditionalFine","inputs":[{"name":"fine","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultKeyRemovalCharge","inputs":[{"name":"keyRemovalCharge","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultPerformanceCoefficients","inputs":[{"name":"attestationsWeight","type":"uint256","internalType":"uint256"},{"name":"blocksWeight","type":"uint256","internalType":"uint256"},{"name":"syncWeight","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultPerformanceLeeway","inputs":[{"name":"leeway","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultPriorityQueueLimit","inputs":[{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultRewardShare","inputs":[{"name":"share","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultStrikesParams","inputs":[{"name":"lifetime","type":"uint256","internalType":"uint256"},{"name":"threshold","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setElRewardsStealingAdditionalFine","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"fine","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setKeyRemovalCharge","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"keyRemovalCharge","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setPerformanceCoefficients","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"attestationsWeight","type":"uint256","internalType":"uint256"},{"name":"blocksWeight","type":"uint256","internalType":"uint256"},{"name":"syncWeight","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setPerformanceLeewayData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"keyPivots","type":"uint256[]","internalType":"uint256[]"},{"name":"performanceLeeways","type":"uint256[]","internalType":"uint256[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setPriorityQueueLimit","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setRewardShareData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"keyPivots","type":"uint256[]","internalType":"uint256[]"},{"name":"rewardShares","type":"uint256[]","internalType":"uint256[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setStrikesParams","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"lifetime","type":"uint256","internalType":"uint256"},{"name":"threshold","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"unsetBadPerformancePenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetElRewardsStealingAdditionalFine","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetKeyRemovalCharge","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetPerformanceCoefficients","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetPerformanceLeewayData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetPriorityQueueLimit","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetRewardShareData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetStrikesParams","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"BadPerformancePenaltySet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"penalty","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BadPerformancePenaltyUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultBadPerformancePenaltySet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultElRewardsStealingAdditionalFineSet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultKeyRemovalChargeSet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultPerformanceCoefficientsSet","inputs":[{"name":"attestationsWeight","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"blocksWeight","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"syncWeight","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultPerformanceLeewaySet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultPriorityQueueLimitSet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultRewardShareSet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultStrikesParamsSet","inputs":[{"name":"lifetime","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"threshold","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ElRewardsStealingAdditionalFineSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"fine","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ElRewardsStealingAdditionalFineUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"KeyRemovalChargeSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"keyRemovalCharge","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"KeyRemovalChargeUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PerformanceCoefficientsSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"attestationsWeight","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"blocksWeight","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"syncWeight","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PerformanceCoefficientsUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PerformanceLeewayDataSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PerformanceLeewayDataUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PriorityQueueLimitSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"limit","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PriorityQueueLimitUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"RewardShareDataSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"RewardShareDataUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"StrikesParamsSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"lifetime","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"threshold","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"StrikesParamsUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidPerformanceLeewayData","inputs":[]},{"type":"error","name":"InvalidRewardShareData","inputs":[]},{"type":"error","name":"InvalidStrikesParams","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"SafeCastOverflowedUintDowncast","inputs":[{"name":"bits","type":"uint8","internalType":"uint8"},{"name":"value","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"ZeroAdminAddress","inputs":[]}] \ No newline at end of file +[{"type":"constructor","inputs":[{"name":"queueLowestPriority","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"QUEUE_LEGACY_PRIORITY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"QUEUE_LOWEST_PRIORITY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultAllowedExitDelay","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultBadPerformancePenalty","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultElRewardsStealingAdditionalFine","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultExitDelayPenalty","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultKeyRemovalCharge","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultKeysLimit","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultMaxWithdrawalRequestFee","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultPerformanceCoefficients","inputs":[],"outputs":[{"name":"attestationsWeight","type":"uint32","internalType":"uint32"},{"name":"blocksWeight","type":"uint32","internalType":"uint32"},{"name":"syncWeight","type":"uint32","internalType":"uint32"}],"stateMutability":"view"},{"type":"function","name":"defaultPerformanceLeeway","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultQueueConfig","inputs":[],"outputs":[{"name":"priority","type":"uint32","internalType":"uint32"},{"name":"maxDeposits","type":"uint32","internalType":"uint32"}],"stateMutability":"view"},{"type":"function","name":"defaultRewardShare","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultStrikesParams","inputs":[],"outputs":[{"name":"lifetime","type":"uint32","internalType":"uint32"},{"name":"threshold","type":"uint32","internalType":"uint32"}],"stateMutability":"view"},{"type":"function","name":"getAllowedExitDelay","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"delay","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBadPerformancePenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"penalty","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getElRewardsStealingAdditionalFine","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"fine","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getExitDelayPenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"penalty","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getKeyRemovalCharge","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"keyRemovalCharge","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getKeysLimit","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"limit","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getMaxWithdrawalRequestFee","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"fee","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getPerformanceCoefficients","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"attestationsWeight","type":"uint256","internalType":"uint256"},{"name":"blocksWeight","type":"uint256","internalType":"uint256"},{"name":"syncWeight","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getPerformanceLeewayData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"data","type":"tuple[]","internalType":"struct ICSParametersRegistry.KeyIndexValueInterval[]","components":[{"name":"minKeyIndex","type":"uint256","internalType":"uint256"},{"name":"value","type":"uint256","internalType":"uint256"}]}],"stateMutability":"view"},{"type":"function","name":"getQueueConfig","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"queuePriority","type":"uint32","internalType":"uint32"},{"name":"maxDeposits","type":"uint32","internalType":"uint32"}],"stateMutability":"view"},{"type":"function","name":"getRewardShareData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"data","type":"tuple[]","internalType":"struct ICSParametersRegistry.KeyIndexValueInterval[]","components":[{"name":"minKeyIndex","type":"uint256","internalType":"uint256"},{"name":"value","type":"uint256","internalType":"uint256"}]}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getStrikesParams","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"lifetime","type":"uint256","internalType":"uint256"},{"name":"threshold","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"admin","type":"address","internalType":"address"},{"name":"data","type":"tuple","internalType":"struct ICSParametersRegistry.InitializationData","components":[{"name":"keyRemovalCharge","type":"uint256","internalType":"uint256"},{"name":"elRewardsStealingAdditionalFine","type":"uint256","internalType":"uint256"},{"name":"keysLimit","type":"uint256","internalType":"uint256"},{"name":"rewardShare","type":"uint256","internalType":"uint256"},{"name":"performanceLeeway","type":"uint256","internalType":"uint256"},{"name":"strikesLifetime","type":"uint256","internalType":"uint256"},{"name":"strikesThreshold","type":"uint256","internalType":"uint256"},{"name":"defaultQueuePriority","type":"uint256","internalType":"uint256"},{"name":"defaultQueueMaxDeposits","type":"uint256","internalType":"uint256"},{"name":"badPerformancePenalty","type":"uint256","internalType":"uint256"},{"name":"attestationsWeight","type":"uint256","internalType":"uint256"},{"name":"blocksWeight","type":"uint256","internalType":"uint256"},{"name":"syncWeight","type":"uint256","internalType":"uint256"},{"name":"defaultAllowedExitDelay","type":"uint256","internalType":"uint256"},{"name":"defaultExitDelayPenalty","type":"uint256","internalType":"uint256"},{"name":"defaultMaxWithdrawalRequestFee","type":"uint256","internalType":"uint256"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setAllowedExitDelay","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"delay","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setBadPerformancePenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"penalty","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultAllowedExitDelay","inputs":[{"name":"delay","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultBadPerformancePenalty","inputs":[{"name":"penalty","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultElRewardsStealingAdditionalFine","inputs":[{"name":"fine","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultExitDelayPenalty","inputs":[{"name":"penalty","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultKeyRemovalCharge","inputs":[{"name":"keyRemovalCharge","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultKeysLimit","inputs":[{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultMaxWithdrawalRequestFee","inputs":[{"name":"fee","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultPerformanceCoefficients","inputs":[{"name":"attestationsWeight","type":"uint256","internalType":"uint256"},{"name":"blocksWeight","type":"uint256","internalType":"uint256"},{"name":"syncWeight","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultPerformanceLeeway","inputs":[{"name":"leeway","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultQueueConfig","inputs":[{"name":"priority","type":"uint256","internalType":"uint256"},{"name":"maxDeposits","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultRewardShare","inputs":[{"name":"share","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultStrikesParams","inputs":[{"name":"lifetime","type":"uint256","internalType":"uint256"},{"name":"threshold","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setElRewardsStealingAdditionalFine","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"fine","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setExitDelayPenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"penalty","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setKeyRemovalCharge","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"keyRemovalCharge","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setKeysLimit","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setMaxWithdrawalRequestFee","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"fee","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setPerformanceCoefficients","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"attestationsWeight","type":"uint256","internalType":"uint256"},{"name":"blocksWeight","type":"uint256","internalType":"uint256"},{"name":"syncWeight","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setPerformanceLeewayData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"data","type":"uint256[2][]","internalType":"uint256[2][]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setQueueConfig","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"priority","type":"uint32","internalType":"uint32"},{"name":"maxDeposits","type":"uint32","internalType":"uint32"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setRewardShareData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"data","type":"uint256[2][]","internalType":"uint256[2][]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setStrikesParams","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"lifetime","type":"uint256","internalType":"uint256"},{"name":"threshold","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"unsetAllowedExitDelay","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetBadPerformancePenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetElRewardsStealingAdditionalFine","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetExitDelayPenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetKeyRemovalCharge","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetKeysLimit","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetMaxWithdrawalRequestFee","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetPerformanceCoefficients","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetPerformanceLeewayData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetQueueConfig","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetRewardShareData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetStrikesParams","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"AllowedExitDelaySet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"delay","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"AllowedExitDelayUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BadPerformancePenaltySet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"penalty","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BadPerformancePenaltyUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultAllowedExitDelaySet","inputs":[{"name":"delay","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultBadPerformancePenaltySet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultElRewardsStealingAdditionalFineSet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultExitDelayPenaltySet","inputs":[{"name":"penalty","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultKeyRemovalChargeSet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultKeysLimitSet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultMaxWithdrawalRequestFeeSet","inputs":[{"name":"fee","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultPerformanceCoefficientsSet","inputs":[{"name":"attestationsWeight","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"blocksWeight","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"syncWeight","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultPerformanceLeewaySet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultQueueConfigSet","inputs":[{"name":"priority","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"maxDeposits","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultRewardShareSet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultStrikesParamsSet","inputs":[{"name":"lifetime","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"threshold","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ElRewardsStealingAdditionalFineSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"fine","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ElRewardsStealingAdditionalFineUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ExitDelayPenaltySet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"penalty","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ExitDelayPenaltyUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"KeyRemovalChargeSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"keyRemovalCharge","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"KeyRemovalChargeUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"KeysLimitSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"limit","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"KeysLimitUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"MaxWithdrawalRequestFeeSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"fee","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"MaxWithdrawalRequestFeeUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PerformanceCoefficientsSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"attestationsWeight","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"blocksWeight","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"syncWeight","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PerformanceCoefficientsUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PerformanceLeewayDataSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"data","type":"uint256[2][]","indexed":false,"internalType":"uint256[2][]"}],"anonymous":false},{"type":"event","name":"PerformanceLeewayDataUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"QueueConfigSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"priority","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"maxDeposits","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"QueueConfigUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"RewardShareDataSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"data","type":"uint256[2][]","indexed":false,"internalType":"uint256[2][]"}],"anonymous":false},{"type":"event","name":"RewardShareDataUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"StrikesParamsSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"lifetime","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"threshold","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"StrikesParamsUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"InvalidExitDelayPenalty","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidKeyIndexValueIntervals","inputs":[]},{"type":"error","name":"InvalidPerformanceCoefficients","inputs":[]},{"type":"error","name":"InvalidPerformanceLeewayData","inputs":[]},{"type":"error","name":"InvalidRewardShareData","inputs":[]},{"type":"error","name":"InvalidStrikesParams","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"QueueCannotBeUsed","inputs":[]},{"type":"error","name":"SafeCastOverflowedUintDowncast","inputs":[{"name":"bits","type":"uint8","internalType":"uint8"},{"name":"value","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroMaxDeposits","inputs":[]}] \ No newline at end of file diff --git a/src/modules/csm/distribution.py b/src/modules/csm/distribution.py index 4c396bd4e..7e091368e 100644 --- a/src/modules/csm/distribution.py +++ b/src/modules/csm/distribution.py @@ -142,10 +142,10 @@ def _calculate_distribution_in_frame( log_operator.performance_coefficients = curve_params.perf_coeffs sorted_active_validators = sorted(active_validators, key=lambda v: v.index) - numbered_validators = enumerate(sorted_active_validators, start=1) - for key_number, validator in numbered_validators: - key_threshold = max(network_perf - curve_params.perf_leeway_data.get_for(key_number), 0) - key_reward_share = curve_params.reward_share_data.get_for(key_number) + indexed_validators = enumerate(sorted_active_validators) + for key_index, validator in indexed_validators: + key_threshold = max(network_perf - curve_params.perf_leeway_data.get_for(key_index), 0) + key_reward_share = curve_params.reward_share_data.get_for(key_index) duties = self.state.get_validator_duties(frame, validator.index) diff --git a/src/providers/execution/contracts/cs_parameters_registry.py b/src/providers/execution/contracts/cs_parameters_registry.py index 4a65f4e9e..8ce5c6495 100644 --- a/src/providers/execution/contracts/cs_parameters_registry.py +++ b/src/providers/execution/contracts/cs_parameters_registry.py @@ -42,35 +42,23 @@ def calc_performance(self, duties: ValidatorDuties) -> float: @dataclass -class RewardShare: - key_pivots: list[int] - reward_shares: list[int] - - def get_for(self, key_number: int) -> float: - if key_number <= 0: - raise ValueError("Key number should be greater than 0") - for i, pivot_number in enumerate(self.key_pivots + [UINT256_MAX]): - if key_number <= pivot_number: - if i >= len(self.reward_shares): - continue - return self.reward_shares[i] / TOTAL_BASIS_POINTS - raise ValueError(f"Can't find performance leeway for {key_number=}. {self=}") +class KeyIndexValueInterval: + minKeyIndex: int + value: int @dataclass -class PerformanceLeeway: - key_pivots: list[int] - performance_leeways: list[int] - - def get_for(self, key_number: int) -> float: - if key_number <= 0: - raise ValueError("Key number should be greater than 0") - for i, pivot_number in enumerate(self.key_pivots + [UINT256_MAX]): - if key_number <= pivot_number: - if i >= len(self.performance_leeways): - continue - return self.performance_leeways[i] / TOTAL_BASIS_POINTS - raise ValueError(f"Can't find performance leeway for {key_number=}. {self=}") +class IntervalMapping: + intervals: list[KeyIndexValueInterval] + + def get_for(self, key_index: int) -> float: + if key_index < 0: + raise ValueError("Key index should be greater than 0 or equal") + for interval in sorted(self.intervals, key=lambda x: x.minKeyIndex, reverse=True): + if key_index >= interval.minKeyIndex: + return interval.value / TOTAL_BASIS_POINTS + raise ValueError(f"No value found for key_index={key_index}") + @dataclass class StrikesParams: @@ -81,8 +69,8 @@ class StrikesParams: @dataclass class CurveParams: perf_coeffs: PerformanceCoefficients - perf_leeway_data: PerformanceLeeway - reward_share_data: RewardShare + perf_leeway_data: IntervalMapping + reward_share_data: IntervalMapping strikes_params: StrikesParams @@ -112,7 +100,7 @@ def get_reward_share_data( self, curve_id: int, block_identifier: BlockIdentifier = "latest", - ) -> RewardShare: + ) -> IntervalMapping: """Returns reward share data for given node operator""" resp = self.functions.getRewardShareData(curve_id).call(block_identifier=block_identifier) @@ -123,14 +111,14 @@ def get_reward_share_data( "block_identifier": repr(block_identifier), } ) - return RewardShare(*resp) + return IntervalMapping(intervals=[KeyIndexValueInterval(r.minKeyIndex, r.value) for r in resp]) @lru_cache() def get_performance_leeway_data( self, curve_id: int, block_identifier: BlockIdentifier = "latest", - ) -> PerformanceLeeway: + ) -> IntervalMapping: """Returns performance leeway data for given node operator""" resp = self.functions.getPerformanceLeewayData(curve_id).call(block_identifier=block_identifier) @@ -141,7 +129,7 @@ def get_performance_leeway_data( "block_identifier": repr(block_identifier), } ) - return PerformanceLeeway(*resp) + return IntervalMapping(intervals=[KeyIndexValueInterval(r.minKeyIndex, r.value) for r in resp]) @lru_cache() def get_strikes_params( diff --git a/tests/integration/contracts/test_cs_parameters_registry.py b/tests/integration/contracts/test_cs_parameters_registry.py index 917b04abc..88a39e80f 100644 --- a/tests/integration/contracts/test_cs_parameters_registry.py +++ b/tests/integration/contracts/test_cs_parameters_registry.py @@ -3,8 +3,7 @@ from src.providers.execution.contracts.cs_parameters_registry import ( PerformanceCoefficients, - PerformanceLeeway, - RewardShare, + IntervalMapping, StrikesParams, ) from tests.integration.contracts.contract_utils import check_contract, check_is_instance_of @@ -17,8 +16,8 @@ def test_cs_parameters_registry(cs_params_contract, caplog): cs_params_contract, [ ("get_performance_coefficients", None, check_is_instance_of(PerformanceCoefficients)), - ("get_reward_share_data", None, check_is_instance_of(RewardShare)), - ("get_performance_leeway_data", None, check_is_instance_of(PerformanceLeeway)), + ("get_reward_share_data", None, check_is_instance_of(IntervalMapping)), + ("get_performance_leeway_data", None, check_is_instance_of(IntervalMapping)), ("get_strikes_params", None, check_is_instance_of(StrikesParams)), ], caplog, diff --git a/tests/modules/csm/test_csm_distribution.py b/tests/modules/csm/test_csm_distribution.py index 677c3b33f..8e0e8b537 100644 --- a/tests/modules/csm/test_csm_distribution.py +++ b/tests/modules/csm/test_csm_distribution.py @@ -6,7 +6,6 @@ from web3.types import Wei from src.constants import TOTAL_BASIS_POINTS -from src.modules.csm.csm import CSOracle from src.modules.csm.distribution import Distribution, ValidatorDuties, ValidatorDutiesOutcome from src.modules.csm.log import FramePerfLog, ValidatorFrameSummary, OperatorFrameSummary from src.modules.csm.state import DutyAccumulator, State, NetworkDuties, Frame @@ -16,8 +15,8 @@ StrikesParams, PerformanceCoefficients, CurveParams, - PerformanceLeeway, - RewardShare, + KeyIndexValueInterval, + IntervalMapping, ) from src.providers.execution.exceptions import InconsistentData from src.types import NodeOperatorId, EpochNumber, ValidatorIndex @@ -605,8 +604,12 @@ def test_calculate_distribution_handles_invalid_distribution_in_total(): side_effect=lambda no_id, _: { NodeOperatorId(5): CurveParams( strikes_params=..., - perf_leeway_data=PerformanceLeeway(key_pivots=[1], performance_leeways=[1000, 2000]), - reward_share_data=RewardShare(key_pivots=[1], reward_shares=[10000, 9000]), + perf_leeway_data=IntervalMapping( + intervals=[KeyIndexValueInterval(0, 1000), KeyIndexValueInterval(1, 2000)] + ), + reward_share_data=IntervalMapping( + intervals=[KeyIndexValueInterval(0, 10000), KeyIndexValueInterval(1, 9000)] + ), perf_coeffs=PerformanceCoefficients(attestations_weight=1, blocks_weight=0, sync_weight=0), ), }.get( @@ -1110,60 +1113,52 @@ def test_performance_coefficients_calc_performance(attestation_perf, proposal_pe @pytest.mark.parametrize( - "key_pivots, performance_leeway, key_number, expected", - [ - ([], [10000], 100500, 10000 / TOTAL_BASIS_POINTS), - ([1], [10000, 9000], 1, 10000 / TOTAL_BASIS_POINTS), - ([1], [10000, 9000], 2, 9000 / TOTAL_BASIS_POINTS), - ([10, 20], [1000, 2000, 3000], 5, 1000 / TOTAL_BASIS_POINTS), - ([10, 20], [1000, 2000, 3000], 10, 1000 / TOTAL_BASIS_POINTS), - ([10, 20], [1000, 2000, 3000], 15, 2000 / TOTAL_BASIS_POINTS), - ([10, 20], [1000, 2000, 3000], 20, 2000 / TOTAL_BASIS_POINTS), - ([10, 20], [1000, 2000, 3000], 25, 3000 / TOTAL_BASIS_POINTS), - ], -) -def test_performance_leeway_returns_correct_reward_share(key_pivots, performance_leeway, key_number, expected): - performance_leeway = PerformanceLeeway(key_pivots, performance_leeway) - assert performance_leeway.get_for(key_number) == expected - - -def test_performance_leeway_raises_error_for_invalid_key_number(): - performance_leeway = PerformanceLeeway([10, 20], [1000, 2000, 3000]) - with pytest.raises(ValueError, match="Key number should be greater than 0"): - performance_leeway.get_for(0) - - -def test_performance_leeway_raises_error_for_key_number_out_of_range(): - performance_leeway = PerformanceLeeway([10], [10000]) - with pytest.raises(ValueError, match="Can't find performance leeway for key_number=40"): - performance_leeway.get_for(40) - - -@pytest.mark.parametrize( - "key_pivots, reward_shares, key_number, expected", + "intervals, key_index, expected", [ - ([], [10000], 100500, 10000 / TOTAL_BASIS_POINTS), - ([1], [10000, 9000], 1, 10000 / TOTAL_BASIS_POINTS), - ([1], [10000, 9000], 2, 9000 / TOTAL_BASIS_POINTS), - ([10, 20], [1000, 2000, 3000], 5, 1000 / TOTAL_BASIS_POINTS), - ([10, 20], [1000, 2000, 3000], 10, 1000 / TOTAL_BASIS_POINTS), - ([10, 20], [1000, 2000, 3000], 15, 2000 / TOTAL_BASIS_POINTS), - ([10, 20], [1000, 2000, 3000], 20, 2000 / TOTAL_BASIS_POINTS), - ([10, 20], [1000, 2000, 3000], 25, 3000 / TOTAL_BASIS_POINTS), + ([KeyIndexValueInterval(0, 10000)], 100500, 10000 / TOTAL_BASIS_POINTS), + ([KeyIndexValueInterval(0, 10000), KeyIndexValueInterval(1, 9000)], 0, 10000 / TOTAL_BASIS_POINTS), + ([KeyIndexValueInterval(0, 10000), KeyIndexValueInterval(1, 9000)], 1, 9000 / TOTAL_BASIS_POINTS), + ( + [KeyIndexValueInterval(0, 1000), KeyIndexValueInterval(10, 2000), KeyIndexValueInterval(20, 3000)], + 4, + 1000 / TOTAL_BASIS_POINTS, + ), + ( + [KeyIndexValueInterval(0, 1000), KeyIndexValueInterval(10, 2000), KeyIndexValueInterval(20, 3000)], + 9, + 1000 / TOTAL_BASIS_POINTS, + ), + ( + [KeyIndexValueInterval(0, 1000), KeyIndexValueInterval(10, 2000), KeyIndexValueInterval(20, 3000)], + 14, + 2000 / TOTAL_BASIS_POINTS, + ), + ( + [KeyIndexValueInterval(0, 1000), KeyIndexValueInterval(10, 2000), KeyIndexValueInterval(20, 3000)], + 19, + 2000 / TOTAL_BASIS_POINTS, + ), + ( + [KeyIndexValueInterval(0, 1000), KeyIndexValueInterval(10, 2000), KeyIndexValueInterval(20, 3000)], + 24, + 3000 / TOTAL_BASIS_POINTS, + ), ], ) -def test_reward_share_returns_correct_reward_share(key_pivots, reward_shares, key_number, expected): - reward_share = RewardShare(key_pivots, reward_shares) - assert reward_share.get_for(key_number) == expected +def test_interval_mapping_returns_correct_reward_share(intervals, key_index, expected): + reward_share = IntervalMapping(intervals=intervals) + assert reward_share.get_for(key_index) == expected -def test_reward_share_raises_error_for_invalid_key_number(): - reward_share = RewardShare([10, 20], [1000, 2000, 3000]) - with pytest.raises(ValueError, match="Key number should be greater than 0"): - reward_share.get_for(0) +def test_interval_mapping_raises_error_for_invalid_key_number(): + reward_share = IntervalMapping( + intervals=[KeyIndexValueInterval(0, 1000), KeyIndexValueInterval(10, 2000), KeyIndexValueInterval(20, 3000)] + ) + with pytest.raises(ValueError, match="Key index should be greater than 0 or equal"): + reward_share.get_for(-1) -def test_reward_share_raises_error_for_key_number_out_of_range(): - reward_share = RewardShare([10], [10000]) - with pytest.raises(ValueError, match="Can't find performance leeway for key_number=40"): - reward_share.get_for(40) +def test_interval_mapping_raises_error_for_key_number_out_of_range(): + reward_share = IntervalMapping(intervals=[KeyIndexValueInterval(10, 10000)]) + with pytest.raises(ValueError, match="No value found for key_index=2"): + reward_share.get_for(2) From cdfd5517555da6d6684481e509fb2f3c097fe2aa Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Thu, 8 May 2025 18:46:57 +0200 Subject: [PATCH 112/162] fix: after merge --- tests/modules/csm/test_state.py | 37 +++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/modules/csm/test_state.py b/tests/modules/csm/test_state.py index 27d386551..fe1fa3c6f 100644 --- a/tests/modules/csm/test_state.py +++ b/tests/modules/csm/test_state.py @@ -233,6 +233,7 @@ def test_increment_prop_duty_creates_new_validator_entry(): assert state.data[frame].proposals[ValidatorIndex(2)].assigned == 1 assert state.data[frame].proposals[ValidatorIndex(2)].included == 1 + @pytest.mark.unit def test_increment_sync_duty_creates_new_validator_entry(): state = State() @@ -246,6 +247,7 @@ def test_increment_sync_duty_creates_new_validator_entry(): assert state.data[frame].syncs[ValidatorIndex(2)].assigned == 1 assert state.data[frame].syncs[ValidatorIndex(2)].included == 1 + @pytest.mark.unit def test_increment_att_duty_handles_non_included_duty(): state = State() @@ -259,6 +261,7 @@ def test_increment_att_duty_handles_non_included_duty(): assert state.data[frame].attestations[ValidatorIndex(1)].assigned == 11 assert state.data[frame].attestations[ValidatorIndex(1)].included == 5 + @pytest.mark.unit def test_increment_prop_duty_handles_non_included_duty(): state = State() @@ -272,6 +275,7 @@ def test_increment_prop_duty_handles_non_included_duty(): assert state.data[frame].proposals[ValidatorIndex(1)].assigned == 11 assert state.data[frame].proposals[ValidatorIndex(1)].included == 5 + @pytest.mark.unit def test_increment_sync_duty_handles_non_included_duty(): state = State() @@ -285,6 +289,7 @@ def test_increment_sync_duty_handles_non_included_duty(): assert state.data[frame].syncs[ValidatorIndex(1)].assigned == 11 assert state.data[frame].syncs[ValidatorIndex(1)].included == 5 + @pytest.mark.unit def test_increment_att_duty_raises_error_for_out_of_range_epoch(): state = State() @@ -296,6 +301,7 @@ def test_increment_att_duty_raises_error_for_out_of_range_epoch(): with pytest.raises(ValueError, match="is out of frames range"): state.increment_att_duty(32, ValidatorIndex(1), True) + @pytest.mark.unit def test_increment_prop_duty_raises_error_for_out_of_range_epoch(): state = State() @@ -307,6 +313,7 @@ def test_increment_prop_duty_raises_error_for_out_of_range_epoch(): with pytest.raises(ValueError, match="is out of frames range"): state.increment_prop_duty(32, ValidatorIndex(1), True) + @pytest.mark.unit def test_increment_sync_duty_raises_error_for_out_of_range_epoch(): state = State() @@ -318,12 +325,14 @@ def test_increment_sync_duty_raises_error_for_out_of_range_epoch(): with pytest.raises(ValueError, match="is out of frames range"): state.increment_sync_duty(32, ValidatorIndex(1), True) + @pytest.mark.unit def test_add_processed_epoch_adds_epoch_to_processed_set(): state = State() state.add_processed_epoch(5) assert 5 in state._processed_epochs + @pytest.mark.unit def test_add_processed_epoch_does_not_duplicate_epochs(): state = State() @@ -331,6 +340,7 @@ def test_add_processed_epoch_does_not_duplicate_epochs(): state.add_processed_epoch(5) assert len(state._processed_epochs) == 1 + @pytest.mark.unit def test_migrate_discards_data_on_version_change(): state = State() @@ -345,6 +355,7 @@ def test_migrate_discards_data_on_version_change(): state.clear.assert_called_once() state.commit.assert_called_once() + @pytest.mark.unit def test_migrate_no_migration_needed(): state = State() @@ -363,6 +374,7 @@ def test_migrate_no_migration_needed(): assert state._consensus_version == 1 state.commit.assert_not_called() + @pytest.mark.unit def test_migrate_migrates_data(): state = State() @@ -395,6 +407,7 @@ def test_migrate_migrates_data(): assert state._consensus_version == 1 state.commit.assert_called_once() + @pytest.mark.unit def test_migrate_invalidates_unmigrated_frames(): state = State() @@ -419,6 +432,7 @@ def test_migrate_invalidates_unmigrated_frames(): assert state._consensus_version == 1 state.commit.assert_called_once() + @pytest.mark.unit def test_migrate_discards_unmigrated_frame(): state = State() @@ -463,6 +477,7 @@ def test_migrate_discards_unmigrated_frame(): assert state._consensus_version == 1 state.commit.assert_called_once() + @pytest.mark.unit def test_migrate_frames_data_creates_new_data_correctly(): state = State() @@ -493,6 +508,7 @@ def test_migrate_frames_data_creates_new_data_correctly(): } assert state._processed_epochs == set(sequence(0, 20)) + @pytest.mark.unit def test_migrate_frames_data_handles_no_migration(): state = State() @@ -518,6 +534,7 @@ def test_migrate_frames_data_handles_no_migration(): } assert state._processed_epochs == set(sequence(0, 20)) + @pytest.mark.unit def test_migrate_frames_data_handles_partial_migration(): state = State() @@ -553,6 +570,7 @@ def test_migrate_frames_data_handles_partial_migration(): } assert state._processed_epochs == set(sequence(0, 20)) + @pytest.mark.unit def test_migrate_frames_data_handles_no_data(): state = State() @@ -564,6 +582,7 @@ def test_migrate_frames_data_handles_no_data(): assert state.data == {(0, 31): NetworkDuties()} + @pytest.mark.unit def test_migrate_frames_data_handles_wider_old_frame(): state = State() @@ -586,6 +605,7 @@ def test_migrate_frames_data_handles_wider_old_frame(): } assert state._processed_epochs == set() + @pytest.mark.unit def test_validate_raises_error_if_state_not_fulfilled(): state = State() @@ -594,6 +614,7 @@ def test_validate_raises_error_if_state_not_fulfilled(): with pytest.raises(InvalidState, match="State is not fulfilled"): state.validate(0, 95) + @pytest.mark.unit def test_validate_raises_error_if_processed_epoch_out_of_range(): state = State() @@ -603,6 +624,7 @@ def test_validate_raises_error_if_processed_epoch_out_of_range(): with pytest.raises(InvalidState, match="Processed epoch 96 is out of range"): state.validate(0, 95) + @pytest.mark.unit def test_validate_raises_error_if_epoch_missing_in_processed_epochs(): state = State() @@ -611,6 +633,7 @@ def test_validate_raises_error_if_epoch_missing_in_processed_epochs(): with pytest.raises(InvalidState, match="Epoch 95 missing in processed epochs"): state.validate(0, 95) + @pytest.mark.unit def test_validate_passes_for_fulfilled_state(): state = State() @@ -618,11 +641,13 @@ def test_validate_passes_for_fulfilled_state(): state._processed_epochs = set(sequence(0, 95)) state.validate(0, 95) + @pytest.mark.unit def test_attestation_aggregate_perf(): aggr = DutyAccumulator(included=333, assigned=777) assert aggr.perf == pytest.approx(0.4285, abs=1e-4) + @pytest.mark.unit def test_get_validator_duties(): state = State() @@ -650,6 +675,7 @@ def test_get_validator_duties(): assert duties.sync.assigned == 3 assert duties.sync.included == 2 + @pytest.mark.unit def test_get_att_network_aggr_computes_correctly(): state = State() @@ -665,6 +691,7 @@ def test_get_att_network_aggr_computes_correctly(): assert aggr.assigned == 30 assert aggr.included == 20 + @pytest.mark.unit def test_get_sync_network_aggr_computes_correctly(): state = State() @@ -680,6 +707,7 @@ def test_get_sync_network_aggr_computes_correctly(): assert aggr.assigned == 30 assert aggr.included == 20 + @pytest.mark.unit def test_get_prop_network_aggr_computes_correctly(): state = State() @@ -695,6 +723,7 @@ def test_get_prop_network_aggr_computes_correctly(): assert aggr.assigned == 30 assert aggr.included == 20 + @pytest.mark.unit def test_get_att_network_aggr_raises_error_for_invalid_accumulator(): state = State() @@ -704,6 +733,7 @@ def test_get_att_network_aggr_raises_error_for_invalid_accumulator(): with pytest.raises(ValueError, match="Invalid accumulator"): state.get_att_network_aggr((0, 31)) + @pytest.mark.unit def test_get_prop_network_aggr_raises_error_for_invalid_accumulator(): state = State() @@ -713,6 +743,7 @@ def test_get_prop_network_aggr_raises_error_for_invalid_accumulator(): with pytest.raises(ValueError, match="Invalid accumulator"): state.get_prop_network_aggr((0, 31)) + @pytest.mark.unit def test_get_sync_network_aggr_raises_error_for_invalid_accumulator(): state = State() @@ -722,24 +753,28 @@ def test_get_sync_network_aggr_raises_error_for_invalid_accumulator(): with pytest.raises(ValueError, match="Invalid accumulator"): state.get_sync_network_aggr((0, 31)) + @pytest.mark.unit def test_get_att_network_aggr_raises_error_for_missing_frame_data(): state = State() with pytest.raises(ValueError, match="No data for frame"): state.get_att_network_aggr((0, 31)) + @pytest.mark.unit def test_get_prop_network_aggr_raises_error_for_missing_frame_data(): state = State() with pytest.raises(ValueError, match="No data for frame"): state.get_prop_network_aggr((0, 31)) + @pytest.mark.unit def test_get_sync_network_aggr_raises_error_for_missing_frame_data(): state = State() with pytest.raises(ValueError, match="No data for frame"): state.get_sync_network_aggr((0, 31)) + @pytest.mark.unit def test_get_att_network_aggr_handles_empty_frame_data(): state = State() @@ -748,6 +783,7 @@ def test_get_att_network_aggr_handles_empty_frame_data(): assert aggr.assigned == 0 assert aggr.included == 0 + @pytest.mark.unit def test_get_prop_network_aggr_handles_empty_frame_data(): state = State() @@ -756,6 +792,7 @@ def test_get_prop_network_aggr_handles_empty_frame_data(): assert aggr.assigned == 0 assert aggr.included == 0 + @pytest.mark.unit def test_get_sync_network_aggr_handles_empty_frame_data(): state = State() From 3633e8f97b3121959e471606c2d086ee5b77df80 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Thu, 8 May 2025 18:53:56 +0200 Subject: [PATCH 113/162] fix: linter --- src/web3py/extensions/csm.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/web3py/extensions/csm.py b/src/web3py/extensions/csm.py index e13db9824..347e5dc52 100644 --- a/src/web3py/extensions/csm.py +++ b/src/web3py/extensions/csm.py @@ -17,6 +17,7 @@ from src.providers.execution.contracts.cs_parameters_registry import CSParametersRegistryContract, CurveParams from src.providers.execution.contracts.cs_strikes import CSStrikesContract from src.providers.ipfs import CID, CIDv0, CIDv1, is_cid_v0 +from src.utils.lazy_object_proxy import LazyObjectProxy from src.types import BlockStamp, NodeOperatorId, SlotNumber logger = logging.getLogger(__name__) From 043d26e78fee2e695363a61648607d68653606a8 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Thu, 8 May 2025 19:11:27 +0200 Subject: [PATCH 114/162] fix: tests --- tests/modules/csm/test_csm_distribution.py | 16 ++++++++++++++++ tests/modules/csm/test_csm_module.py | 2 +- tests/modules/csm/test_state.py | 2 ++ tests/modules/csm/test_strikes.py | 10 ++++++++++ 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/tests/modules/csm/test_csm_distribution.py b/tests/modules/csm/test_csm_distribution.py index 8e0e8b537..bd5190ee3 100644 --- a/tests/modules/csm/test_csm_distribution.py +++ b/tests/modules/csm/test_csm_distribution.py @@ -277,6 +277,7 @@ ), ], ) +@pytest.mark.unit def test_calculate_distribution( frames: list[Frame], last_report, @@ -313,6 +314,7 @@ def test_calculate_distribution( assert log.frame == frames[i] +@pytest.mark.unit def test_calculate_distribution_handles_invalid_distribution(): # Mocking the data from EL w3 = Mock(spec=Web3, csm=Mock(spec=CSM, fee_distributor=Mock(spec=CSFeeDistributorContract))) @@ -340,6 +342,7 @@ def test_calculate_distribution_handles_invalid_distribution(): distribution.calculate(..., Mock(strikes={}, rewards=[])) +@pytest.mark.unit def test_calculate_distribution_handles_invalid_distribution_in_total(): # Mocking the data from EL w3 = Mock(spec=Web3, csm=Mock(spec=CSM, fee_distributor=Mock(spec=CSFeeDistributorContract))) @@ -782,6 +785,7 @@ def test_calculate_distribution_handles_invalid_distribution_in_total(): ), ], ) +@pytest.mark.unit def test_calculate_distribution_in_frame( to_distribute, frame_validators, @@ -835,6 +839,7 @@ def test_calculate_distribution_in_frame( (0.95, 0.5, 0.7, pytest.approx(0.8859, rel=1e-4)), ], ) +@pytest.mark.unit def test_get_network_performance(att_perf, prop_perf, sync_perf, expected): distribution = Distribution(Mock(), Mock(), Mock()) distribution.state.get_att_network_aggr = Mock(return_value=Mock(perf=att_perf) if att_perf is not None else None) @@ -851,6 +856,7 @@ def test_get_network_performance(att_perf, prop_perf, sync_perf, expected): assert result == expected +@pytest.mark.unit def test_get_network_performance_raises_error_for_invalid_performance(): distribution = Distribution(Mock(), Mock(), Mock()) distribution.state.get_att_network_aggr = Mock(return_value=Mock(perf=1.1)) @@ -907,6 +913,7 @@ def test_get_network_performance_raises_error_for_invalid_performance(): ), ], ) +@pytest.mark.unit def test_process_validator_duty(validator_duties, is_slashed, threshold, reward_share, expected_outcome): validator = LidoValidatorFactory.build() validator.validator.slashed = is_slashed @@ -974,6 +981,7 @@ def test_process_validator_duty(validator_duties, is_slashed, threshold, reward_ ), ], ) +@pytest.mark.unit def test_calc_rewards_distribution_in_frame( participation_shares, rewards_to_distribute, rebate_share, expected_distribution ): @@ -983,6 +991,7 @@ def test_calc_rewards_distribution_in_frame( assert rewards_distribution == expected_distribution +@pytest.mark.unit def test_calc_rewards_distribution_in_frame_handles_negative_to_distribute(): participation_shares = {NodeOperatorId(1): 100, NodeOperatorId(2): 200} rewards_to_distribute = Wei(-1) @@ -1052,6 +1061,7 @@ def test_calc_rewards_distribution_in_frame_handles_negative_to_distribute(): ), ], ) +@pytest.mark.unit def test_merge_strikes( acc: dict, strikes_in_frame: dict, @@ -1076,6 +1086,7 @@ def test_merge_strikes( (50, 50, 100), ], ) +@pytest.mark.unit def tests_validates_correct_distribution(total_distributed_rewards, total_rebate, total_rewards_to_distribute): Distribution.validate_distribution(total_distributed_rewards, total_rebate, total_rewards_to_distribute) @@ -1087,6 +1098,7 @@ def tests_validates_correct_distribution(total_distributed_rewards, total_rebate (200, 0, 199), ], ) +@pytest.mark.unit def test_raises_error_for_invalid_distribution(total_distributed_rewards, total_rebate, total_rewards_to_distribute): with pytest.raises(ValueError, match="Invalid distribution"): Distribution.validate_distribution(total_distributed_rewards, total_rebate, total_rewards_to_distribute) @@ -1102,6 +1114,7 @@ def test_raises_error_for_invalid_distribution(total_distributed_rewards, total_ (1, 1, 1, 1), ], ) +@pytest.mark.unit def test_performance_coefficients_calc_performance(attestation_perf, proposal_perf, sync_perf, expected): performance_coefficients = PerformanceCoefficients() duties = ValidatorDuties( @@ -1145,11 +1158,13 @@ def test_performance_coefficients_calc_performance(attestation_perf, proposal_pe ), ], ) +@pytest.mark.unit def test_interval_mapping_returns_correct_reward_share(intervals, key_index, expected): reward_share = IntervalMapping(intervals=intervals) assert reward_share.get_for(key_index) == expected +@pytest.mark.unit def test_interval_mapping_raises_error_for_invalid_key_number(): reward_share = IntervalMapping( intervals=[KeyIndexValueInterval(0, 1000), KeyIndexValueInterval(10, 2000), KeyIndexValueInterval(20, 3000)] @@ -1158,6 +1173,7 @@ def test_interval_mapping_raises_error_for_invalid_key_number(): reward_share.get_for(-1) +@pytest.mark.unit def test_interval_mapping_raises_error_for_key_number_out_of_range(): reward_share = IntervalMapping(intervals=[KeyIndexValueInterval(10, 10000)]) with pytest.raises(ValueError, match="No value found for key_index=2"): diff --git a/tests/modules/csm/test_csm_module.py b/tests/modules/csm/test_csm_module.py index 3f7d690ae..d4f08f9f4 100644 --- a/tests/modules/csm/test_csm_module.py +++ b/tests/modules/csm/test_csm_module.py @@ -30,7 +30,7 @@ def mock_load_state(monkeypatch: pytest.MonkeyPatch): @pytest.fixture() -def module(web3, csm: CSM): +def module(web3): yield CSOracle(web3) diff --git a/tests/modules/csm/test_state.py b/tests/modules/csm/test_state.py index fe1fa3c6f..c9cdaf08c 100644 --- a/tests/modules/csm/test_state.py +++ b/tests/modules/csm/test_state.py @@ -33,9 +33,11 @@ def cache_path(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: monkeypatch.setattr(variables, "CACHE_PATH", tmp_path) return tmp_path + @pytest.mark.unit def test_file_returns_correct_path(self, cache_path: Path): assert State.file() == cache_path / "cache.pkl" + @pytest.mark.unit def test_buffer_returns_correct_path(self, cache_path: Path): state = State() assert state.buffer == cache_path / "cache.buf" diff --git a/tests/modules/csm/test_strikes.py b/tests/modules/csm/test_strikes.py index 8f67ef6d2..1ec3e7c66 100644 --- a/tests/modules/csm/test_strikes.py +++ b/tests/modules/csm/test_strikes.py @@ -1,8 +1,10 @@ +import pytest from eth_utils.types import is_list_like from src.modules.csm.types import StrikesList +@pytest.mark.unit def test_create_empty(): strikes = StrikesList([]) assert not len(strikes) @@ -11,45 +13,53 @@ def test_create_empty(): assert not len(strikes) +@pytest.mark.unit def test_create_not_empty(): strikes = StrikesList([1, 2, 3]) assert strikes == [1, 2, 3] +@pytest.mark.unit def test_eq(): assert StrikesList([1, 2, 3]) == StrikesList([1, 2, 3]) assert StrikesList([1, 2, 3]) != StrikesList([1, 2]) assert StrikesList([1, 2, 3]) == [1, 2, 3] +@pytest.mark.unit def test_create_maxlen_smaller_than_iterable(): strikes = StrikesList([1, 2, 3], maxlen=5) assert strikes == [1, 2, 3, 0, 0] +@pytest.mark.unit def test_create_maxlen_larger_than_iterable(): strikes = StrikesList([1, 2, 3], maxlen=2) assert strikes == [1, 2] +@pytest.mark.unit def test_create_resize_to_smaller(): strikes = StrikesList([1, 2, 3]) strikes.resize(2) assert strikes == [1, 2] +@pytest.mark.unit def test_create_resize_to_larger(): strikes = StrikesList([1, 2, 3]) strikes.resize(5) assert strikes == [1, 2, 3, 0, 0] +@pytest.mark.unit def test_push_element(): strikes = StrikesList([1, 2, 3]) strikes.push(4) assert strikes == [4, 1, 2, 3] +@pytest.mark.unit def test_is_list_like(): strikes = StrikesList([1, 2, 3]) assert is_list_like(strikes) From a1b6486dac6b9ca60bdecd7900410970de71f8b8 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Thu, 8 May 2025 20:05:59 +0200 Subject: [PATCH 115/162] fix: tests --- tests/modules/csm/test_checkpoint.py | 12 ++++++++++-- tests/modules/csm/test_csm_module.py | 3 +-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/modules/csm/test_checkpoint.py b/tests/modules/csm/test_checkpoint.py index 929ac2dc6..45776d419 100644 --- a/tests/modules/csm/test_checkpoint.py +++ b/tests/modules/csm/test_checkpoint.py @@ -469,7 +469,11 @@ def test_get_sync_committee_fetches_and_caches_when_not_cached(frame_checkpoint_ frame_checkpoint_processor.converter.get_epoch_first_slot = Mock(return_value=SlotNumber(0)) frame_checkpoint_processor.cc.get_sync_committee = Mock(return_value=sync_committee) - result = frame_checkpoint_processor._get_sync_committee(epoch) + prev_slot_response = Mock() + prev_slot_response.message.slot = SlotNumber(0) + prev_slot_response.message.body.execution_payload.block_hash = "0x00" + with patch('src.modules.csm.checkpoint.get_prev_non_missed_slot', Mock(return_value=prev_slot_response)): + result = frame_checkpoint_processor._get_sync_committee(epoch) assert result.validators == sync_committee.validators assert SYNC_COMMITTEES_CACHE[sync_committee_period].validators == sync_committee.validators @@ -489,7 +493,11 @@ def test_get_sync_committee_handles_cache_eviction(frame_checkpoint_processor): cache.max_size = 1 cache[old_sync_committee_period] = old_sync_committee - result = frame_checkpoint_processor._get_sync_committee(epoch) + prev_slot_response = Mock() + prev_slot_response.message.slot = SlotNumber(0) + prev_slot_response.message.body.execution_payload.block_hash = "0x00" + with patch('src.modules.csm.checkpoint.get_prev_non_missed_slot', Mock(return_value=prev_slot_response)): + result = frame_checkpoint_processor._get_sync_committee(epoch) assert result == sync_committee assert sync_committee_period in SYNC_COMMITTEES_CACHE diff --git a/tests/modules/csm/test_csm_module.py b/tests/modules/csm/test_csm_module.py index d4f08f9f4..b1ccb2fcf 100644 --- a/tests/modules/csm/test_csm_module.py +++ b/tests/modules/csm/test_csm_module.py @@ -18,7 +18,6 @@ from src.providers.ipfs import CID from src.types import NodeOperatorId, SlotNumber from src.utils.types import hex_str_to_bytes -from src.web3py.extensions.csm import CSM from src.web3py.types import Web3 from tests.factory.blockstamp import ReferenceBlockStampFactory from tests.factory.configs import ChainConfigFactory, FrameConfigFactory @@ -765,7 +764,7 @@ def test_make_strikes_tree(module: CSOracle, param: StrikesTreeTestParam): class TestLastReport: - @pytest.mark.usefixtures("csm") + @pytest.mark.unit def test_load(self, web3: Web3): blockstamp = Mock() From 5ede0c652eae9aa8c45fb361e9e4afa7e55ea17f Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Thu, 8 May 2025 20:32:28 +0200 Subject: [PATCH 116/162] fix: use `to_0x_hex` in encoder --- src/modules/csm/helpers/last_report.py | 4 ++-- src/modules/csm/tree.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/modules/csm/helpers/last_report.py b/src/modules/csm/helpers/last_report.py index 9752ffd58..7c57b054d 100644 --- a/src/modules/csm/helpers/last_report.py +++ b/src/modules/csm/helpers/last_report.py @@ -54,7 +54,7 @@ def rewards(self) -> Iterable[RewardsTreeLeaf]: if (self.rewards_tree_cid is None) != (self.rewards_tree_root == ZERO_HASH): raise InconsistentData( "Got inconsistent previous rewards tree data: " - f"tree_root={self.rewards_tree_root.hex()} tree_cid={self.rewards_tree_cid=}" + f"tree_root={self.rewards_tree_root.to_0x_hex()} tree_cid={self.rewards_tree_cid=}" ) if self.rewards_tree_cid is None or self.rewards_tree_root == ZERO_HASH: @@ -76,7 +76,7 @@ def strikes(self) -> dict[StrikesValidator, StrikesList]: if (self.strikes_tree_cid is None) != (self.strikes_tree_root == ZERO_HASH): raise InconsistentData( "Got inconsistent previous strikes tree data: " - f"tree_root={self.strikes_tree_root.hex()} tree_cid={self.strikes_tree_cid=}" + f"tree_root={self.strikes_tree_root.to_0x_hex()} tree_cid={self.strikes_tree_cid=}" ) if self.strikes_tree_cid is None or self.strikes_tree_root == ZERO_HASH: diff --git a/src/modules/csm/tree.py b/src/modules/csm/tree.py index 42083bbdb..03732e209 100644 --- a/src/modules/csm/tree.py +++ b/src/modules/csm/tree.py @@ -13,7 +13,7 @@ class TreeJSONEncoder(JSONEncoder): def default(self, o): if isinstance(o, bytes): - return HexBytes(o).hex() + return HexBytes(o).to_0x_hex() return super().default(o) From eff02e09dd167c07fb521440613290a9e97626fc Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Thu, 8 May 2025 20:42:54 +0200 Subject: [PATCH 117/162] feat: run fork tests on v6 branch before merge --- .github/workflows/mainnet_fork_tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/mainnet_fork_tests.yml b/.github/workflows/mainnet_fork_tests.yml index 7f7412a4e..d436fa62b 100644 --- a/.github/workflows/mainnet_fork_tests.yml +++ b/.github/workflows/mainnet_fork_tests.yml @@ -11,6 +11,7 @@ on: branches: - main - develop + - feat/oracle-v6 paths: - "src/**" From 12713ec6b34002235ee37753c4b4a06b6c7e12e5 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Thu, 8 May 2025 21:18:32 +0200 Subject: [PATCH 118/162] fix: action --- .github/workflows/mainnet_fork_tests.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/mainnet_fork_tests.yml b/.github/workflows/mainnet_fork_tests.yml index d436fa62b..203795e86 100644 --- a/.github/workflows/mainnet_fork_tests.yml +++ b/.github/workflows/mainnet_fork_tests.yml @@ -48,9 +48,7 @@ jobs: cache-dependency-path: "testruns/community-staking-module/yarn.lock" - name: Install Just - uses: extractions/setup-just@v2 - with: - just-version: '1.24.0' + run: cargo install "just@1.24.0" - name: Install dependencies working-directory: testruns/community-staking-module From d90df7d5f5893ef2183e92c497366d9635acb34d Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Fri, 9 May 2025 12:36:02 +0200 Subject: [PATCH 119/162] fix: upgrade to v2 --- tests/fork/test_csm_oracle_cycle.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/fork/test_csm_oracle_cycle.py b/tests/fork/test_csm_oracle_cycle.py index 7ca79eb6f..d7c72451a 100644 --- a/tests/fork/test_csm_oracle_cycle.py +++ b/tests/fork/test_csm_oracle_cycle.py @@ -84,8 +84,7 @@ def update_csm_to_v2(accounts_from_fork, forked_el_client: Web3, anvil_port: int 'CHAIN': chain, "ANVIL_PORT": str(anvil_port), "RPC_URL": f"http://127.0.0.1:{anvil_port}", # FIXME: actually unused by the script, remove when fixed - 'DEPLOY_CONFIG': f'./artifacts/{chain}/deploy-{chain}.json', - 'UPGRADE_CONFIG': f'./artifacts/local/upgrade-{chain}.json', + 'DEPLOY_CONFIG': f'./artifacts/local/upgrade-{chain}.json', }, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, @@ -133,7 +132,11 @@ def missed_initial_frame(frame_config: FrameConfig): ) @pytest.mark.parametrize( 'running_finalized_slots', - [start_before_initial_epoch, start_after_initial_epoch, missed_initial_frame], + [ + start_before_initial_epoch, + # start_after_initial_epoch, + # missed_initial_frame + ], indirect=True, ) def test_csm_module_report(module, set_oracle_members, running_finalized_slots, account_from): From 4cb97252bd27eaf6b0fcc54a5fc1ad39904cf9dc Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Wed, 14 May 2025 16:27:06 +0200 Subject: [PATCH 120/162] fix: `32` -> `slots_per_epoch` --- src/modules/csm/checkpoint.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/modules/csm/checkpoint.py b/src/modules/csm/checkpoint.py index 48e548c41..a506e9e47 100644 --- a/src/modules/csm/checkpoint.py +++ b/src/modules/csm/checkpoint.py @@ -187,7 +187,8 @@ def _select_block_roots( block_root = self._select_block_root_by_slot(block_roots, checkpoint_slot, slot_to_check) roots_to_check.append((slot_to_check, block_root)) - duty_epoch_roots, next_epoch_roots = roots_to_check[:32], roots_to_check[32:] + slots_per_epoch = self.converter.chain_config.slots_per_epoch + duty_epoch_roots, next_epoch_roots = roots_to_check[:slots_per_epoch], roots_to_check[slots_per_epoch:] return duty_epoch_roots, next_epoch_roots From 07126ec3761d972014ad00a0968f0c8b1147e7e0 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Wed, 14 May 2025 16:32:01 +0200 Subject: [PATCH 121/162] feat: add `DistributionResult` --- src/modules/csm/distribution.py | 53 +++++++++++----------- tests/modules/csm/test_csm_distribution.py | 14 +++--- 2 files changed, 33 insertions(+), 34 deletions(-) diff --git a/src/modules/csm/distribution.py b/src/modules/csm/distribution.py index 7e091368e..b499072e9 100644 --- a/src/modules/csm/distribution.py +++ b/src/modules/csm/distribution.py @@ -1,7 +1,7 @@ import logging import math from collections import defaultdict -from dataclasses import dataclass +from dataclasses import dataclass, field from src.modules.csm.helpers.last_report import LastReport from src.modules.csm.log import FramePerfLog, OperatorFrameSummary @@ -25,32 +25,29 @@ class ValidatorDutiesOutcome: strikes: int +@dataclass +class DistributionResult: + total_rewards: Shares = 0 + total_rebate: Shares = 0 + total_rewards_map: dict[NodeOperatorId, Shares] = field(default_factory=dict) + strikes: dict[StrikesValidator, StrikesList] = field(default_factory=dict) + logs: list[FramePerfLog] = field(default_factory=list) + + class Distribution: w3: Web3 converter: Web3Converter state: State - total_rewards: Shares - total_rewards_map: defaultdict[NodeOperatorId, Shares] - total_rebate: Shares - strikes: dict[StrikesValidator, StrikesList] - logs: list[FramePerfLog] - def __init__(self, w3: Web3, converter: Web3Converter, state: State): self.w3 = w3 self.converter = converter self.state = state - # Distribution results - self.total_rewards = 0 - self.total_rewards_map = defaultdict[NodeOperatorId, int](int) - self.total_rebate = 0 - self.strikes = {} - self.logs: list[FramePerfLog] = [] - - def calculate(self, blockstamp: ReferenceBlockStamp, last_report: LastReport) -> None: - """Computes distribution of fee shares at the given timestamp""" - self.strikes.update(last_report.strikes.items()) + def calculate(self, blockstamp: ReferenceBlockStamp, last_report: LastReport) -> DistributionResult: + """Computes distribution of fee shares at the given timestamp""" + result = DistributionResult() + result.strikes.update(last_report.strikes.items()) for frame in self.state.frames: from_epoch, to_epoch = frame @@ -60,7 +57,7 @@ def calculate(self, blockstamp: ReferenceBlockStamp, last_report: LastReport) -> frame_module_validators = self._get_module_validators(frame_blockstamp) total_rewards_to_distribute = self.w3.csm.fee_distributor.shares_to_distribute(frame_blockstamp.block_hash) - distributed_so_far = self.total_rewards + self.total_rebate + distributed_so_far = result.total_rewards + result.total_rebate rewards_to_distribute_in_frame = total_rewards_to_distribute - distributed_so_far frame_log = FramePerfLog(frame_blockstamp, frame) @@ -75,25 +72,27 @@ def calculate(self, blockstamp: ReferenceBlockStamp, last_report: LastReport) -> if not distributed_rewards_in_frame: logger.info({"msg": f"No rewards distributed in frame [{from_epoch};{to_epoch}]"}) - self._merge_strikes(self.strikes, strikes_in_frame, frame_blockstamp) + self._merge_strikes(result.strikes, strikes_in_frame, frame_blockstamp) if not strikes_in_frame: logger.info({"msg": f"No strikes in frame [{from_epoch};{to_epoch}]"}) - self.total_rewards += distributed_rewards_in_frame - self.total_rebate += rebate_to_protocol_in_frame + result.total_rewards += distributed_rewards_in_frame + result.total_rebate += rebate_to_protocol_in_frame - self.validate_distribution(self.total_rewards, self.total_rebate, total_rewards_to_distribute) + self.validate_distribution(result.total_rewards, result.total_rebate, total_rewards_to_distribute) for no_id, rewards in rewards_map_in_frame.items(): - self.total_rewards_map[no_id] += rewards + result.total_rewards_map[no_id] += rewards - self.logs.append(frame_log) + result.logs.append(frame_log) - if self.total_rewards != sum(self.total_rewards_map.values()): - raise InconsistentData(f"Invalid distribution: {sum(self.total_rewards_map.values())=} != {self.total_rewards=}") + if result.total_rewards != sum(result.total_rewards_map.values()): + raise InconsistentData(f"Invalid distribution: {sum(result.total_rewards_map.values())=} != {result.total_rewards=}") for no_id, last_report_rewards in last_report.rewards: - self.total_rewards_map[no_id] += last_report_rewards + result.total_rewards_map[no_id] += last_report_rewards + + return result def _get_frame_blockstamp(self, blockstamp: ReferenceBlockStamp, to_epoch: EpochNumber) -> ReferenceBlockStamp: if to_epoch != blockstamp.ref_epoch: diff --git a/tests/modules/csm/test_csm_distribution.py b/tests/modules/csm/test_csm_distribution.py index bd5190ee3..d3c935b8e 100644 --- a/tests/modules/csm/test_csm_distribution.py +++ b/tests/modules/csm/test_csm_distribution.py @@ -301,15 +301,15 @@ def test_calculate_distribution( distribution._get_frame_blockstamp = Mock(side_effect=frame_blockstamps) distribution._calculate_distribution_in_frame = Mock(side_effect=distribution_in_frame) - distribution.calculate(blockstamp=..., last_report=last_report) + result = distribution.calculate(blockstamp=..., last_report=last_report) - assert distribution.total_rewards == expected_total_rewards - assert distribution.total_rewards_map == expected_total_rewards_map - assert distribution.total_rebate == expected_total_rebate - assert distribution.strikes == expected_strikes + assert result.total_rewards == expected_total_rewards + assert result.total_rewards_map == expected_total_rewards_map + assert result.total_rebate == expected_total_rebate + assert result.strikes == expected_strikes - assert len(distribution.logs) == len(frames) - for i, log in enumerate(distribution.logs): + assert len(result.logs) == len(frames) + for i, log in enumerate(result.logs): assert log.blockstamp == frame_blockstamps[i] assert log.frame == frames[i] From 19a32ceeeff996ab0c1d01d870213d27a35c5821 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Wed, 14 May 2025 16:35:42 +0200 Subject: [PATCH 122/162] fix: attrs --- src/modules/csm/csm.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index a067a9057..c11fae2b2 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -10,7 +10,7 @@ ) from src.metrics.prometheus.duration_meter import duration_meter from src.modules.csm.checkpoint import FrameCheckpointProcessor, FrameCheckpointsIterator, MinStepIsNotReached -from src.modules.csm.distribution import Distribution, StrikesValidator +from src.modules.csm.distribution import Distribution, StrikesValidator, DistributionResult from src.modules.csm.helpers.last_report import LastReport from src.modules.csm.log import FramePerfLog from src.modules.csm.state import State @@ -129,10 +129,10 @@ def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: def _get_last_report(self, blockstamp: BlockStamp) -> LastReport: return LastReport.load(self.w3, blockstamp) - def calculate_distribution(self, blockstamp: ReferenceBlockStamp, last_report: LastReport) -> Distribution: + def calculate_distribution(self, blockstamp: ReferenceBlockStamp, last_report: LastReport) -> DistributionResult: distribution = Distribution(self.w3, self.converter(blockstamp), self.state) - distribution.calculate(blockstamp, last_report) - return distribution + result = distribution.calculate(blockstamp, last_report) + return result def is_main_data_submitted(self, blockstamp: BlockStamp) -> bool: last_ref_slot = self.w3.csm.get_csm_last_processing_ref_slot(blockstamp) From 2d8a620c5ad0cd66a7411b4e1ab597b87faebe71 Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Wed, 14 May 2025 17:05:40 +0200 Subject: [PATCH 123/162] refactor: _merge_strikes to return value --- src/modules/csm/distribution.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/modules/csm/distribution.py b/src/modules/csm/distribution.py index b499072e9..d68e37ff1 100644 --- a/src/modules/csm/distribution.py +++ b/src/modules/csm/distribution.py @@ -1,6 +1,7 @@ import logging import math from collections import defaultdict +from copy import deepcopy from dataclasses import dataclass, field from src.modules.csm.helpers.last_report import LastReport @@ -9,7 +10,7 @@ from src.modules.csm.types import Shares, StrikesList, StrikesValidator from src.providers.execution.contracts.cs_parameters_registry import PerformanceCoefficients from src.providers.execution.exceptions import InconsistentData -from src.types import NodeOperatorId, ReferenceBlockStamp, EpochNumber, StakingModuleAddress +from src.types import EpochNumber, NodeOperatorId, ReferenceBlockStamp, StakingModuleAddress from src.utils.slot import get_reference_blockstamp from src.utils.web3converter import Web3Converter from src.web3py.extensions.lido_validators import LidoValidator, ValidatorsByNodeOperator @@ -72,7 +73,7 @@ def calculate(self, blockstamp: ReferenceBlockStamp, last_report: LastReport) -> if not distributed_rewards_in_frame: logger.info({"msg": f"No rewards distributed in frame [{from_epoch};{to_epoch}]"}) - self._merge_strikes(result.strikes, strikes_in_frame, frame_blockstamp) + result.strikes = self._process_strikes(result.strikes, strikes_in_frame, frame_blockstamp) if not strikes_in_frame: logger.info({"msg": f"No strikes in frame [{from_epoch};{to_epoch}]"}) @@ -267,23 +268,27 @@ def validate_distribution(total_distributed_rewards, total_rebate, total_rewards f"Invalid distribution: {total_distributed_rewards + total_rebate} > {total_rewards_to_distribute}" ) - def _merge_strikes( + def _process_strikes( self, acc: dict[StrikesValidator, StrikesList], strikes_in_frame: dict[StrikesValidator, int], frame_blockstamp: ReferenceBlockStamp, - ) -> None: + ) -> dict[StrikesValidator, StrikesList]: + merged = deepcopy(acc) + for key in strikes_in_frame: - if key not in acc: - acc[key] = StrikesList() - acc[key].push(strikes_in_frame[key]) + if key not in merged: + merged[key] = StrikesList() + merged[key].push(strikes_in_frame[key]) - for key in list(acc.keys()): + for key in list(merged.keys()): no_id, _ = key if key not in strikes_in_frame: - acc[key].push(StrikesList.SENTINEL) # Just shifting... + merged[key].push(StrikesList.SENTINEL) # Just shifting... maxlen = self.w3.csm.get_curve_params(no_id, frame_blockstamp).strikes_params.lifetime - acc[key].resize(maxlen) + merged[key].resize(maxlen) # NOTE: Cleanup sequences like [0,0,0] since they don't bring any information. - if not sum(acc[key]): - del acc[key] + if not sum(merged[key]): + del merged[key] + + return merged From e303ebca6ae65b54f70dc93ca804117289135422 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Wed, 14 May 2025 17:07:17 +0200 Subject: [PATCH 124/162] fix: use `defaultdict` --- src/modules/csm/distribution.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/modules/csm/distribution.py b/src/modules/csm/distribution.py index d68e37ff1..874397757 100644 --- a/src/modules/csm/distribution.py +++ b/src/modules/csm/distribution.py @@ -30,8 +30,8 @@ class ValidatorDutiesOutcome: class DistributionResult: total_rewards: Shares = 0 total_rebate: Shares = 0 - total_rewards_map: dict[NodeOperatorId, Shares] = field(default_factory=dict) - strikes: dict[StrikesValidator, StrikesList] = field(default_factory=dict) + total_rewards_map: dict[NodeOperatorId, Shares] = field(default_factory=lambda: defaultdict(Shares)) + strikes: dict[StrikesValidator, StrikesList] = field(default_factory=lambda: defaultdict(StrikesList)) logs: list[FramePerfLog] = field(default_factory=list) @@ -62,13 +62,10 @@ def calculate(self, blockstamp: ReferenceBlockStamp, last_report: LastReport) -> rewards_to_distribute_in_frame = total_rewards_to_distribute - distributed_so_far frame_log = FramePerfLog(frame_blockstamp, frame) - ( - rewards_map_in_frame, - distributed_rewards_in_frame, - rebate_to_protocol_in_frame, - strikes_in_frame - ) = self._calculate_distribution_in_frame( - frame, frame_blockstamp, rewards_to_distribute_in_frame, frame_module_validators, frame_log + (rewards_map_in_frame, distributed_rewards_in_frame, rebate_to_protocol_in_frame, strikes_in_frame) = ( + self._calculate_distribution_in_frame( + frame, frame_blockstamp, rewards_to_distribute_in_frame, frame_module_validators, frame_log + ) ) if not distributed_rewards_in_frame: logger.info({"msg": f"No rewards distributed in frame [{from_epoch};{to_epoch}]"}) @@ -88,7 +85,9 @@ def calculate(self, blockstamp: ReferenceBlockStamp, last_report: LastReport) -> result.logs.append(frame_log) if result.total_rewards != sum(result.total_rewards_map.values()): - raise InconsistentData(f"Invalid distribution: {sum(result.total_rewards_map.values())=} != {result.total_rewards=}") + raise InconsistentData( + f"Invalid distribution: {sum(result.total_rewards_map.values())=} != {result.total_rewards=}" + ) for no_id, last_report_rewards in last_report.rewards: result.total_rewards_map[no_id] += last_report_rewards From 78b743c577ff9a626dbb58d749eaaf43985ed8dd Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Wed, 14 May 2025 17:28:23 +0200 Subject: [PATCH 125/162] test: fix test --- tests/modules/csm/test_csm_distribution.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/modules/csm/test_csm_distribution.py b/tests/modules/csm/test_csm_distribution.py index d3c935b8e..7147a9582 100644 --- a/tests/modules/csm/test_csm_distribution.py +++ b/tests/modules/csm/test_csm_distribution.py @@ -1073,9 +1073,9 @@ def test_merge_strikes( side_effect=lambda no_id, _: Mock(strikes_params=threshold_per_op[no_id]) ) - distribution._merge_strikes(acc, strikes_in_frame, frame_blockstamp=Mock()) + result = distribution._process_strikes(acc, strikes_in_frame, frame_blockstamp=Mock()) - assert acc == expected + assert result == expected @pytest.mark.parametrize( From d749020b481976059e77136b962dfcabc94ce09b Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Thu, 15 May 2025 23:22:34 +0200 Subject: [PATCH 126/162] fix: revert commented tests --- tests/fork/test_csm_oracle_cycle.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/fork/test_csm_oracle_cycle.py b/tests/fork/test_csm_oracle_cycle.py index d7c72451a..7c42c1cdb 100644 --- a/tests/fork/test_csm_oracle_cycle.py +++ b/tests/fork/test_csm_oracle_cycle.py @@ -132,11 +132,7 @@ def missed_initial_frame(frame_config: FrameConfig): ) @pytest.mark.parametrize( 'running_finalized_slots', - [ - start_before_initial_epoch, - # start_after_initial_epoch, - # missed_initial_frame - ], + [start_before_initial_epoch, start_after_initial_epoch, missed_initial_frame], indirect=True, ) def test_csm_module_report(module, set_oracle_members, running_finalized_slots, account_from): From b30f92574303493a9ba89ae03a015ec55c00d50a Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Mon, 19 May 2025 10:37:06 +0200 Subject: [PATCH 127/162] feat: new key number intervals implementation --- src/modules/csm/distribution.py | 8 ++--- .../contracts/cs_parameters_registry.py | 22 +++++++------- tests/modules/csm/test_csm_distribution.py | 30 +++++++++---------- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/modules/csm/distribution.py b/src/modules/csm/distribution.py index 874397757..077cd8bf4 100644 --- a/src/modules/csm/distribution.py +++ b/src/modules/csm/distribution.py @@ -141,10 +141,10 @@ def _calculate_distribution_in_frame( log_operator.performance_coefficients = curve_params.perf_coeffs sorted_active_validators = sorted(active_validators, key=lambda v: v.index) - indexed_validators = enumerate(sorted_active_validators) - for key_index, validator in indexed_validators: - key_threshold = max(network_perf - curve_params.perf_leeway_data.get_for(key_index), 0) - key_reward_share = curve_params.reward_share_data.get_for(key_index) + numbered_validators = enumerate(sorted_active_validators, 1) + for key_number, validator in numbered_validators: + key_threshold = max(network_perf - curve_params.perf_leeway_data.get_for(key_number), 0) + key_reward_share = curve_params.reward_share_data.get_for(key_number) duties = self.state.get_validator_duties(frame, validator.index) diff --git a/src/providers/execution/contracts/cs_parameters_registry.py b/src/providers/execution/contracts/cs_parameters_registry.py index 8ce5c6495..0b460e09c 100644 --- a/src/providers/execution/contracts/cs_parameters_registry.py +++ b/src/providers/execution/contracts/cs_parameters_registry.py @@ -42,22 +42,22 @@ def calc_performance(self, duties: ValidatorDuties) -> float: @dataclass -class KeyIndexValueInterval: - minKeyIndex: int +class KeyNumberValueInterval: + minKeyNumber: int value: int @dataclass class IntervalMapping: - intervals: list[KeyIndexValueInterval] + intervals: list[KeyNumberValueInterval] - def get_for(self, key_index: int) -> float: - if key_index < 0: - raise ValueError("Key index should be greater than 0 or equal") - for interval in sorted(self.intervals, key=lambda x: x.minKeyIndex, reverse=True): - if key_index >= interval.minKeyIndex: + def get_for(self, key_number: int) -> float: + if key_number < 1: + raise ValueError("Key number should be greater than 1 or equal") + for interval in sorted(self.intervals, key=lambda x: x.minKeyNumber, reverse=True): + if key_number >= interval.minKeyNumber: return interval.value / TOTAL_BASIS_POINTS - raise ValueError(f"No value found for key_index={key_index}") + raise ValueError(f"No value found for key number={key_number}") @dataclass @@ -111,7 +111,7 @@ def get_reward_share_data( "block_identifier": repr(block_identifier), } ) - return IntervalMapping(intervals=[KeyIndexValueInterval(r.minKeyIndex, r.value) for r in resp]) + return IntervalMapping(intervals=[KeyNumberValueInterval(r.minKeyNumber, r.value) for r in resp]) @lru_cache() def get_performance_leeway_data( @@ -129,7 +129,7 @@ def get_performance_leeway_data( "block_identifier": repr(block_identifier), } ) - return IntervalMapping(intervals=[KeyIndexValueInterval(r.minKeyIndex, r.value) for r in resp]) + return IntervalMapping(intervals=[KeyNumberValueInterval(r.minKeyNumber, r.value) for r in resp]) @lru_cache() def get_strikes_params( diff --git a/tests/modules/csm/test_csm_distribution.py b/tests/modules/csm/test_csm_distribution.py index 7147a9582..26fcca1c0 100644 --- a/tests/modules/csm/test_csm_distribution.py +++ b/tests/modules/csm/test_csm_distribution.py @@ -15,7 +15,7 @@ StrikesParams, PerformanceCoefficients, CurveParams, - KeyIndexValueInterval, + KeyNumberValueInterval, IntervalMapping, ) from src.providers.execution.exceptions import InconsistentData @@ -608,10 +608,10 @@ def test_calculate_distribution_handles_invalid_distribution_in_total(): NodeOperatorId(5): CurveParams( strikes_params=..., perf_leeway_data=IntervalMapping( - intervals=[KeyIndexValueInterval(0, 1000), KeyIndexValueInterval(1, 2000)] + intervals=[KeyNumberValueInterval(1, 1000), KeyNumberValueInterval(2, 2000)] ), reward_share_data=IntervalMapping( - intervals=[KeyIndexValueInterval(0, 10000), KeyIndexValueInterval(1, 9000)] + intervals=[KeyNumberValueInterval(1, 10000), KeyNumberValueInterval(2, 9000)] ), perf_coeffs=PerformanceCoefficients(attestations_weight=1, blocks_weight=0, sync_weight=0), ), @@ -1128,31 +1128,31 @@ def test_performance_coefficients_calc_performance(attestation_perf, proposal_pe @pytest.mark.parametrize( "intervals, key_index, expected", [ - ([KeyIndexValueInterval(0, 10000)], 100500, 10000 / TOTAL_BASIS_POINTS), - ([KeyIndexValueInterval(0, 10000), KeyIndexValueInterval(1, 9000)], 0, 10000 / TOTAL_BASIS_POINTS), - ([KeyIndexValueInterval(0, 10000), KeyIndexValueInterval(1, 9000)], 1, 9000 / TOTAL_BASIS_POINTS), + ([KeyNumberValueInterval(1, 10000)], 100500, 10000 / TOTAL_BASIS_POINTS), + ([KeyNumberValueInterval(1, 10000), KeyNumberValueInterval(2, 9000)], 1, 10000 / TOTAL_BASIS_POINTS), + ([KeyNumberValueInterval(1, 10000), KeyNumberValueInterval(2, 9000)], 2, 9000 / TOTAL_BASIS_POINTS), ( - [KeyIndexValueInterval(0, 1000), KeyIndexValueInterval(10, 2000), KeyIndexValueInterval(20, 3000)], + [KeyNumberValueInterval(1, 1000), KeyNumberValueInterval(11, 2000), KeyNumberValueInterval(21, 3000)], 4, 1000 / TOTAL_BASIS_POINTS, ), ( - [KeyIndexValueInterval(0, 1000), KeyIndexValueInterval(10, 2000), KeyIndexValueInterval(20, 3000)], + [KeyNumberValueInterval(1, 1000), KeyNumberValueInterval(11, 2000), KeyNumberValueInterval(21, 3000)], 9, 1000 / TOTAL_BASIS_POINTS, ), ( - [KeyIndexValueInterval(0, 1000), KeyIndexValueInterval(10, 2000), KeyIndexValueInterval(20, 3000)], + [KeyNumberValueInterval(1, 1000), KeyNumberValueInterval(11, 2000), KeyNumberValueInterval(21, 3000)], 14, 2000 / TOTAL_BASIS_POINTS, ), ( - [KeyIndexValueInterval(0, 1000), KeyIndexValueInterval(10, 2000), KeyIndexValueInterval(20, 3000)], + [KeyNumberValueInterval(1, 1000), KeyNumberValueInterval(11, 2000), KeyNumberValueInterval(21, 3000)], 19, 2000 / TOTAL_BASIS_POINTS, ), ( - [KeyIndexValueInterval(0, 1000), KeyIndexValueInterval(10, 2000), KeyIndexValueInterval(20, 3000)], + [KeyNumberValueInterval(1, 1000), KeyNumberValueInterval(11, 2000), KeyNumberValueInterval(21, 3000)], 24, 3000 / TOTAL_BASIS_POINTS, ), @@ -1167,14 +1167,14 @@ def test_interval_mapping_returns_correct_reward_share(intervals, key_index, exp @pytest.mark.unit def test_interval_mapping_raises_error_for_invalid_key_number(): reward_share = IntervalMapping( - intervals=[KeyIndexValueInterval(0, 1000), KeyIndexValueInterval(10, 2000), KeyIndexValueInterval(20, 3000)] + intervals=[KeyNumberValueInterval(1, 1000), KeyNumberValueInterval(11, 2000), KeyNumberValueInterval(21, 3000)] ) - with pytest.raises(ValueError, match="Key index should be greater than 0 or equal"): + with pytest.raises(ValueError, match="Key number should be greater than 1 or equal"): reward_share.get_for(-1) @pytest.mark.unit def test_interval_mapping_raises_error_for_key_number_out_of_range(): - reward_share = IntervalMapping(intervals=[KeyIndexValueInterval(10, 10000)]) - with pytest.raises(ValueError, match="No value found for key_index=2"): + reward_share = IntervalMapping(intervals=[KeyNumberValueInterval(11, 10000)]) + with pytest.raises(ValueError, match="No value found for key number=2"): reward_share.get_for(2) From 95675ecd40fea04cbb24f63648d03e6c9fd6dae5 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Mon, 19 May 2025 10:44:43 +0200 Subject: [PATCH 128/162] fix: linter after merge --- tests/integration/contracts/test_lido.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/integration/contracts/test_lido.py b/tests/integration/contracts/test_lido.py index c1dfad075..b76e72b40 100644 --- a/tests/integration/contracts/test_lido.py +++ b/tests/integration/contracts/test_lido.py @@ -5,13 +5,13 @@ @pytest.mark.integration -def test_lido_contract_call(lido_contract, accounting_oracle_contract, caplog): +def test_lido_contract_call(lido_contract, accounting_oracle_contract, burner_contract, caplog): check_contract( lido_contract, [ - ('get_buffered_ether', None, check_value_type(int)), - ('total_supply', None, check_value_type(int)), - ('get_beacon_stat', None, check_value_type(BeaconStat)), + ('get_buffered_ether', None, lambda response: check_value_type(response, int)), + ('total_supply', None, lambda response: check_value_type(response, int)), + ('get_beacon_stat', None, lambda response: check_value_type(response, BeaconStat)), ( 'handle_oracle_report', ( @@ -27,7 +27,7 @@ def test_lido_contract_call(lido_contract, accounting_oracle_contract, caplog): # Call depends on contract state '0xffa34bcc5a08c92272a62e591f7afb9cb839134aa08c091ae0c95682fba35da9', ), - check_value_type(LidoReportRebase), + lambda response: check_value_type(response, LidoReportRebase), ), ], caplog, From d41619d55d9c9bc6285933a0bb8455c5b7953f20 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Mon, 19 May 2025 11:17:11 +0200 Subject: [PATCH 129/162] fix: new abis, strikes address --- assets/CSAccounting.json | 2 +- assets/CSFeeDistributor.json | 2 +- assets/CSFeeOracle.json | 2 +- assets/CSModule.json | 2 +- assets/CSParametersRegistry.json | 2 +- assets/CSStrikes.json | 2 +- src/providers/execution/contracts/cs_fee_oracle.py | 4 ++-- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/assets/CSAccounting.json b/assets/CSAccounting.json index 348937605..9effc63b3 100644 --- a/assets/CSAccounting.json +++ b/assets/CSAccounting.json @@ -1 +1 @@ -[{"type":"function","name":"chargeFee","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"chargeRecipient","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"claimRewardsStETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stETHAmount","type":"uint256","internalType":"uint256"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"claimRewardsUnstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stEthAmount","type":"uint256","internalType":"uint256"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"claimRewardsWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"wstETHAmount","type":"uint256","internalType":"uint256"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"compensateLockedBondETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"depositETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"depositStETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stETHAmount","type":"uint256","internalType":"uint256"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"depositWstETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"wstETHAmount","type":"uint256","internalType":"uint256"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"feeDistributor","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSFeeDistributor"}],"stateMutability":"view"},{"type":"function","name":"getActualLockedBond","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBond","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondAmountByKeysCount","inputs":[{"name":"keys","type":"uint256","internalType":"uint256"},{"name":"curve","type":"tuple","internalType":"struct ICSBondCurve.BondCurve","components":[{"name":"points","type":"uint256[]","internalType":"uint256[]"},{"name":"trend","type":"uint256","internalType":"uint256"}]}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondAmountByKeysCount","inputs":[{"name":"keys","type":"uint256","internalType":"uint256"},{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondAmountByKeysCountWstETH","inputs":[{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondAmountByKeysCountWstETH","inputs":[{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"curve","type":"tuple","internalType":"struct ICSBondCurve.BondCurve","components":[{"name":"points","type":"uint256[]","internalType":"uint256[]"},{"name":"trend","type":"uint256","internalType":"uint256"}]}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondCurve","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct ICSBondCurve.BondCurve","components":[{"name":"points","type":"uint256[]","internalType":"uint256[]"},{"name":"trend","type":"uint256","internalType":"uint256"}]}],"stateMutability":"view"},{"type":"function","name":"getBondCurveId","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondLockRetentionPeriod","inputs":[],"outputs":[{"name":"retention","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondShares","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getCurveInfo","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct ICSBondCurve.BondCurve","components":[{"name":"points","type":"uint256[]","internalType":"uint256[]"},{"name":"trend","type":"uint256","internalType":"uint256"}]}],"stateMutability":"view"},{"type":"function","name":"getKeysCountByBondAmount","inputs":[{"name":"amount","type":"uint256","internalType":"uint256"},{"name":"curve","type":"tuple","internalType":"struct ICSBondCurve.BondCurve","components":[{"name":"points","type":"uint256[]","internalType":"uint256[]"},{"name":"trend","type":"uint256","internalType":"uint256"}]}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getKeysCountByBondAmount","inputs":[{"name":"amount","type":"uint256","internalType":"uint256"},{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getLockedBondInfo","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct ICSBondLock.BondLock","components":[{"name":"amount","type":"uint128","internalType":"uint128"},{"name":"retentionUntil","type":"uint128","internalType":"uint128"}]}],"stateMutability":"view"},{"type":"function","name":"getRequiredBondForNextKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"additionalKeys","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRequiredBondForNextKeysWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"additionalKeys","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getUnbondedKeysCount","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getUnbondedKeysCountToEject","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"lockBondETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"penalize","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"releaseLockedBondETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resetBondCurve","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setBondCurve","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"settleLockedBondETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"totalBondShares","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"}] +[{"type":"constructor","inputs":[{"name":"lidoLocator","type":"address","internalType":"address"},{"name":"module","type":"address","internalType":"address"},{"name":"_feeDistributor","type":"address","internalType":"address"},{"name":"maxCurveLength","type":"uint256","internalType":"uint256"},{"name":"minBondLockPeriod","type":"uint256","internalType":"uint256"},{"name":"maxBondLockPeriod","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"DEFAULT_BOND_CURVE_ID","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"FEE_DISTRIBUTOR","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSFeeDistributor"}],"stateMutability":"view"},{"type":"function","name":"LIDO","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ILido"}],"stateMutability":"view"},{"type":"function","name":"LIDO_LOCATOR","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ILidoLocator"}],"stateMutability":"view"},{"type":"function","name":"MANAGE_BOND_CURVES_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"MAX_BOND_LOCK_PERIOD","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"MAX_CURVE_LENGTH","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"MIN_BOND_LOCK_PERIOD","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"MIN_CURVE_LENGTH","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"MODULE","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSModule"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_INFINITELY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RECOVERER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RESUME_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"SET_BOND_CURVE_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"WITHDRAWAL_QUEUE","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IWithdrawalQueue"}],"stateMutability":"view"},{"type":"function","name":"WSTETH","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IWstETH"}],"stateMutability":"view"},{"type":"function","name":"addBondCurve","inputs":[{"name":"bondCurve","type":"tuple[]","internalType":"struct ICSBondCurve.BondCurveIntervalInput[]","components":[{"name":"minKeysCount","type":"uint256","internalType":"uint256"},{"name":"trend","type":"uint256","internalType":"uint256"}]}],"outputs":[{"name":"id","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"chargeFee","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"chargePenaltyRecipient","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"claimRewardsStETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stETHAmount","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"claimedShares","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"claimRewardsUnstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stEthAmount","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"requestId","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"claimRewardsWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"wstETHAmount","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"claimedWstETH","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"compensateLockedBondETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"depositETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"depositETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"depositStETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stETHAmount","type":"uint256","internalType":"uint256"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"depositStETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stETHAmount","type":"uint256","internalType":"uint256"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"depositWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"wstETHAmount","type":"uint256","internalType":"uint256"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"depositWstETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"wstETHAmount","type":"uint256","internalType":"uint256"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"feeDistributor","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSFeeDistributor"}],"stateMutability":"view"},{"type":"function","name":"finalizeUpgradeV2","inputs":[{"name":"bondCurvesInputs","type":"tuple[][]","internalType":"struct ICSBondCurve.BondCurveIntervalInput[][]","components":[{"name":"minKeysCount","type":"uint256","internalType":"uint256"},{"name":"trend","type":"uint256","internalType":"uint256"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"getActualLockedBond","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBond","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondAmountByKeysCount","inputs":[{"name":"keys","type":"uint256","internalType":"uint256"},{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondAmountByKeysCountWstETH","inputs":[{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondCurve","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct ICSBondCurve.BondCurve","components":[{"name":"intervals","type":"tuple[]","internalType":"struct ICSBondCurve.BondCurveInterval[]","components":[{"name":"minKeysCount","type":"uint256","internalType":"uint256"},{"name":"minBond","type":"uint256","internalType":"uint256"},{"name":"trend","type":"uint256","internalType":"uint256"}]}]}],"stateMutability":"view"},{"type":"function","name":"getBondCurveId","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondLockPeriod","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondShares","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondSummary","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"current","type":"uint256","internalType":"uint256"},{"name":"required","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondSummaryShares","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"current","type":"uint256","internalType":"uint256"},{"name":"required","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getClaimableBondShares","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getClaimableRewardsAndBondShares","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"claimableShares","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getCurveInfo","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct ICSBondCurve.BondCurve","components":[{"name":"intervals","type":"tuple[]","internalType":"struct ICSBondCurve.BondCurveInterval[]","components":[{"name":"minKeysCount","type":"uint256","internalType":"uint256"},{"name":"minBond","type":"uint256","internalType":"uint256"},{"name":"trend","type":"uint256","internalType":"uint256"}]}]}],"stateMutability":"view"},{"type":"function","name":"getCurvesCount","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getInitializedVersion","inputs":[],"outputs":[{"name":"","type":"uint64","internalType":"uint64"}],"stateMutability":"view"},{"type":"function","name":"getKeysCountByBondAmount","inputs":[{"name":"amount","type":"uint256","internalType":"uint256"},{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getLockedBondInfo","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct ICSBondLock.BondLock","components":[{"name":"amount","type":"uint128","internalType":"uint128"},{"name":"until","type":"uint128","internalType":"uint128"}]}],"stateMutability":"view"},{"type":"function","name":"getRequiredBondForNextKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"additionalKeys","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRequiredBondForNextKeysWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"additionalKeys","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getResumeSinceTimestamp","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getUnbondedKeysCount","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getUnbondedKeysCountToEject","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"bondCurve","type":"tuple[]","internalType":"struct ICSBondCurve.BondCurveIntervalInput[]","components":[{"name":"minKeysCount","type":"uint256","internalType":"uint256"},{"name":"trend","type":"uint256","internalType":"uint256"}]},{"name":"admin","type":"address","internalType":"address"},{"name":"bondLockPeriod","type":"uint256","internalType":"uint256"},{"name":"_chargePenaltyRecipient","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"isPaused","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"lockBondETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"pauseFor","inputs":[{"name":"duration","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"penalize","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"pullFeeRewards","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC1155","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC20","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC721","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverEther","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverStETHShares","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"releaseLockedBondETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renewBurnerAllowance","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resume","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setBondCurve","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setBondLockPeriod","inputs":[{"name":"period","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setChargePenaltyRecipient","inputs":[{"name":"_chargePenaltyRecipient","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"settleLockedBondETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"applied","type":"bool","internalType":"bool"}],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"totalBondShares","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"updateBondCurve","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"bondCurve","type":"tuple[]","internalType":"struct ICSBondCurve.BondCurveIntervalInput[]","components":[{"name":"minKeysCount","type":"uint256","internalType":"uint256"},{"name":"trend","type":"uint256","internalType":"uint256"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"BondBurned","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"toBurnAmount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"burnedAmount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondCharged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"toChargeAmount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"chargedAmount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondClaimedStETH","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"to","type":"address","indexed":false,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondClaimedUnstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"to","type":"address","indexed":false,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"requestId","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondClaimedWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"to","type":"address","indexed":false,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondCurveAdded","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"bondCurveIntervals","type":"tuple[]","indexed":false,"internalType":"struct ICSBondCurve.BondCurveIntervalInput[]","components":[{"name":"minKeysCount","type":"uint256","internalType":"uint256"},{"name":"trend","type":"uint256","internalType":"uint256"}]}],"anonymous":false},{"type":"event","name":"BondCurveSet","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"curveId","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondCurveUpdated","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"bondCurveIntervals","type":"tuple[]","indexed":false,"internalType":"struct ICSBondCurve.BondCurveIntervalInput[]","components":[{"name":"minKeysCount","type":"uint256","internalType":"uint256"},{"name":"trend","type":"uint256","internalType":"uint256"}]}],"anonymous":false},{"type":"event","name":"BondDepositedETH","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"from","type":"address","indexed":false,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondDepositedStETH","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"from","type":"address","indexed":false,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondDepositedWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"from","type":"address","indexed":false,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondLockChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"newAmount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"until","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondLockCompensated","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondLockPeriodChanged","inputs":[{"name":"period","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondLockRemoved","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ChargePenaltyRecipientSet","inputs":[{"name":"chargePenaltyRecipient","type":"address","indexed":false,"internalType":"address"}],"anonymous":false},{"type":"event","name":"ERC1155Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC20Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC721Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"EtherRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"Paused","inputs":[{"name":"duration","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Resumed","inputs":[],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"StETHSharesRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"ElRewardsVaultReceiveFailed","inputs":[]},{"type":"error","name":"FailedToSendEther","inputs":[]},{"type":"error","name":"InvalidBondCurveId","inputs":[]},{"type":"error","name":"InvalidBondCurveLength","inputs":[]},{"type":"error","name":"InvalidBondCurveMaxLength","inputs":[]},{"type":"error","name":"InvalidBondCurveValues","inputs":[]},{"type":"error","name":"InvalidBondCurvesLength","inputs":[]},{"type":"error","name":"InvalidBondLockAmount","inputs":[]},{"type":"error","name":"InvalidBondLockPeriod","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidInitializationCurveId","inputs":[]},{"type":"error","name":"NodeOperatorDoesNotExist","inputs":[]},{"type":"error","name":"NotAllowedToRecover","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"NothingToClaim","inputs":[]},{"type":"error","name":"PauseUntilMustBeInFuture","inputs":[]},{"type":"error","name":"PausedExpected","inputs":[]},{"type":"error","name":"ResumedExpected","inputs":[]},{"type":"error","name":"SafeCastOverflowedUintDowncast","inputs":[{"name":"bits","type":"uint8","internalType":"uint8"},{"name":"value","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"SenderIsNotEligible","inputs":[]},{"type":"error","name":"SenderIsNotModule","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroChargePenaltyRecipientAddress","inputs":[]},{"type":"error","name":"ZeroFeeDistributorAddress","inputs":[]},{"type":"error","name":"ZeroLocatorAddress","inputs":[]},{"type":"error","name":"ZeroModuleAddress","inputs":[]},{"type":"error","name":"ZeroPauseDuration","inputs":[]}] \ No newline at end of file diff --git a/assets/CSFeeDistributor.json b/assets/CSFeeDistributor.json index da1cfa3ed..df3d2142f 100644 --- a/assets/CSFeeDistributor.json +++ b/assets/CSFeeDistributor.json @@ -1 +1 @@ -[{"type":"constructor","inputs":[{"name":"stETH","type":"address","internalType":"address"},{"name":"accounting","type":"address","internalType":"address"},{"name":"oracle","type":"address","internalType":"address"}],"stateMutability":"nonpayable"},{"type":"function","name":"ACCOUNTING","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"ORACLE","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"RECOVERER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"STETH","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IStETH"}],"stateMutability":"view"},{"type":"function","name":"distributeFees","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"shares","type":"uint256","internalType":"uint256"},{"name":"proof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"sharesToDistribute","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"distributedShares","inputs":[{"name":"","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getFeesToDistribute","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"shares","type":"uint256","internalType":"uint256"},{"name":"proof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"sharesToDistribute","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"hashLeaf","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"shares","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"pure"},{"type":"function","name":"initialize","inputs":[{"name":"admin","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"pendingSharesToDistribute","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"processOracleReport","inputs":[{"name":"_treeRoot","type":"bytes32","internalType":"bytes32"},{"name":"_treeCid","type":"string","internalType":"string"},{"name":"distributed","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC1155","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC20","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC721","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverEther","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"totalClaimableShares","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"treeCid","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"treeRoot","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"event","name":"DistributionDataUpdated","inputs":[{"name":"totalClaimableShares","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"treeRoot","type":"bytes32","indexed":false,"internalType":"bytes32"},{"name":"treeCid","type":"string","indexed":false,"internalType":"string"}],"anonymous":false},{"type":"event","name":"FeeDistributed","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"FeeSharesDecrease","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidProof","inputs":[]},{"type":"error","name":"InvalidShares","inputs":[]},{"type":"error","name":"InvalidTreeCID","inputs":[]},{"type":"error","name":"InvalidTreeRoot","inputs":[]},{"type":"error","name":"NotAccounting","inputs":[]},{"type":"error","name":"NotAllowedToRecover","inputs":[]},{"type":"error","name":"NotEnoughShares","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"NotOracle","inputs":[]},{"type":"error","name":"ZeroAccountingAddress","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroOracleAddress","inputs":[]},{"type":"error","name":"ZeroStEthAddress","inputs":[]}] +[{"type":"constructor","inputs":[{"name":"stETH","type":"address","internalType":"address"},{"name":"accounting","type":"address","internalType":"address"},{"name":"oracle","type":"address","internalType":"address"}],"stateMutability":"nonpayable"},{"type":"function","name":"ACCOUNTING","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"ORACLE","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"RECOVERER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"STETH","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IStETH"}],"stateMutability":"view"},{"type":"function","name":"distributeFees","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"shares","type":"uint256","internalType":"uint256"},{"name":"proof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"sharesToDistribute","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"distributedShares","inputs":[{"name":"","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"distributionDataHistoryCount","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"finalizeUpgradeV2","inputs":[{"name":"_rebateRecipient","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"getFeesToDistribute","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"shares","type":"uint256","internalType":"uint256"},{"name":"proof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"sharesToDistribute","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getHistoricalDistributionData","inputs":[{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct ICSFeeDistributor.DistributionData","components":[{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"treeRoot","type":"bytes32","internalType":"bytes32"},{"name":"treeCid","type":"string","internalType":"string"},{"name":"logCid","type":"string","internalType":"string"},{"name":"distributed","type":"uint256","internalType":"uint256"},{"name":"rebate","type":"uint256","internalType":"uint256"}]}],"stateMutability":"view"},{"type":"function","name":"getInitializedVersion","inputs":[],"outputs":[{"name":"","type":"uint64","internalType":"uint64"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"hashLeaf","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"shares","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"pure"},{"type":"function","name":"initialize","inputs":[{"name":"admin","type":"address","internalType":"address"},{"name":"_rebateRecipient","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"logCid","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"pendingSharesToDistribute","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"processOracleReport","inputs":[{"name":"_treeRoot","type":"bytes32","internalType":"bytes32"},{"name":"_treeCid","type":"string","internalType":"string"},{"name":"_logCid","type":"string","internalType":"string"},{"name":"distributed","type":"uint256","internalType":"uint256"},{"name":"rebate","type":"uint256","internalType":"uint256"},{"name":"refSlot","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"rebateRecipient","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"recoverERC1155","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC20","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC721","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverEther","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setRebateRecipient","inputs":[{"name":"_rebateRecipient","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"totalClaimableShares","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"treeCid","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"treeRoot","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"event","name":"DistributionDataUpdated","inputs":[{"name":"totalClaimableShares","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"treeRoot","type":"bytes32","indexed":false,"internalType":"bytes32"},{"name":"treeCid","type":"string","indexed":false,"internalType":"string"}],"anonymous":false},{"type":"event","name":"DistributionLogUpdated","inputs":[{"name":"logCid","type":"string","indexed":false,"internalType":"string"}],"anonymous":false},{"type":"event","name":"ERC1155Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC20Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC721Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"EtherRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"ModuleFeeDistributed","inputs":[{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"OperatorFeeDistributed","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"RebateRecipientSet","inputs":[{"name":"recipient","type":"address","indexed":false,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RebateTransferred","inputs":[{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"StETHSharesRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"FailedToSendEther","inputs":[]},{"type":"error","name":"FeeSharesDecrease","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidLogCID","inputs":[]},{"type":"error","name":"InvalidProof","inputs":[]},{"type":"error","name":"InvalidReportData","inputs":[]},{"type":"error","name":"InvalidShares","inputs":[]},{"type":"error","name":"InvalidTreeCid","inputs":[]},{"type":"error","name":"InvalidTreeRoot","inputs":[]},{"type":"error","name":"NotAccounting","inputs":[]},{"type":"error","name":"NotAllowedToRecover","inputs":[]},{"type":"error","name":"NotEnoughShares","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"NotOracle","inputs":[]},{"type":"error","name":"ZeroAccountingAddress","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroOracleAddress","inputs":[]},{"type":"error","name":"ZeroRebateRecipientAddress","inputs":[]},{"type":"error","name":"ZeroStEthAddress","inputs":[]}] \ No newline at end of file diff --git a/assets/CSFeeOracle.json b/assets/CSFeeOracle.json index 419541b27..f01871eb6 100644 --- a/assets/CSFeeOracle.json +++ b/assets/CSFeeOracle.json @@ -1 +1 @@ -[{"type":"constructor","inputs":[{"name":"secondsPerSlot","type":"uint256","internalType":"uint256"},{"name":"genesisTime","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"GENESIS_TIME","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"MANAGE_CONSENSUS_CONTRACT_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"MANAGE_CONSENSUS_VERSION_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_INFINITELY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RECOVERER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RESUME_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"SECONDS_PER_SLOT","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"SUBMIT_DATA_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"discardConsensusReport","inputs":[{"name":"refSlot","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"feeDistributor","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSFeeDistributor"}],"stateMutability":"view"},{"type":"function","name":"finalizeUpgradeV2","inputs":[{"name":"consensusVersion","type":"uint256","internalType":"uint256"},{"name":"strikesContract","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"getConsensusContract","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getConsensusReport","inputs":[],"outputs":[{"name":"hash","type":"bytes32","internalType":"bytes32"},{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"processingDeadlineTime","type":"uint256","internalType":"uint256"},{"name":"processingStarted","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"getConsensusVersion","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getContractVersion","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getLastProcessingRefSlot","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getResumeSinceTimestamp","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"admin","type":"address","internalType":"address"},{"name":"feeDistributorContract","type":"address","internalType":"address"},{"name":"strikesContract","type":"address","internalType":"address"},{"name":"consensusContract","type":"address","internalType":"address"},{"name":"consensusVersion","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"isPaused","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"pauseFor","inputs":[{"name":"duration","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"pauseUntil","inputs":[{"name":"pauseUntilInclusive","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC1155","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC20","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC721","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverEther","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resume","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setConsensusContract","inputs":[{"name":"addr","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setConsensusVersion","inputs":[{"name":"version","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setFeeDistributorContract","inputs":[{"name":"feeDistributorContract","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setStrikesContract","inputs":[{"name":"strikesContract","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"strikes","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSStrikes"}],"stateMutability":"view"},{"type":"function","name":"submitConsensusReport","inputs":[{"name":"reportHash","type":"bytes32","internalType":"bytes32"},{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"submitReportData","inputs":[{"name":"data","type":"tuple","internalType":"struct ICSFeeOracle.ReportData","components":[{"name":"consensusVersion","type":"uint256","internalType":"uint256"},{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"treeRoot","type":"bytes32","internalType":"bytes32"},{"name":"treeCid","type":"string","internalType":"string"},{"name":"logCid","type":"string","internalType":"string"},{"name":"distributed","type":"uint256","internalType":"uint256"},{"name":"rebate","type":"uint256","internalType":"uint256"},{"name":"strikesTreeRoot","type":"bytes32","internalType":"bytes32"},{"name":"strikesTreeCid","type":"string","internalType":"string"}]},{"name":"contractVersion","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"event","name":"ConsensusHashContractSet","inputs":[{"name":"addr","type":"address","indexed":true,"internalType":"address"},{"name":"prevAddr","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"ConsensusVersionSet","inputs":[{"name":"version","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"prevVersion","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ContractVersionSet","inputs":[{"name":"version","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC1155Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC20Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC721Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"EtherRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"FeeDistributorContractSet","inputs":[{"name":"feeDistributorContract","type":"address","indexed":false,"internalType":"address"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"Paused","inputs":[{"name":"duration","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ProcessingStarted","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"hash","type":"bytes32","indexed":false,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"ReportDiscarded","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"hash","type":"bytes32","indexed":false,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"ReportSubmitted","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"hash","type":"bytes32","indexed":false,"internalType":"bytes32"},{"name":"processingDeadlineTime","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Resumed","inputs":[],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"StETHSharesRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"StrikesContractSet","inputs":[{"name":"strikesContract","type":"address","indexed":false,"internalType":"address"}],"anonymous":false},{"type":"event","name":"WarnProcessingMissed","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"AddressCannotBeSame","inputs":[]},{"type":"error","name":"AddressCannotBeZero","inputs":[]},{"type":"error","name":"FailedToSendEther","inputs":[]},{"type":"error","name":"HashCannotBeZero","inputs":[]},{"type":"error","name":"InitialRefSlotCannotBeLessThanProcessingOne","inputs":[{"name":"initialRefSlot","type":"uint256","internalType":"uint256"},{"name":"processingRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"InvalidContractVersion","inputs":[]},{"type":"error","name":"InvalidContractVersionIncrement","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"NoConsensusReportToProcess","inputs":[]},{"type":"error","name":"NonZeroContractVersionOnInit","inputs":[]},{"type":"error","name":"NotAllowedToRecover","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"PauseUntilMustBeInFuture","inputs":[]},{"type":"error","name":"PausedExpected","inputs":[]},{"type":"error","name":"ProcessingDeadlineMissed","inputs":[{"name":"deadline","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"RefSlotAlreadyProcessing","inputs":[]},{"type":"error","name":"RefSlotCannotDecrease","inputs":[{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"prevRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"RefSlotMustBeGreaterThanProcessingOne","inputs":[{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"processingRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"ResumedExpected","inputs":[]},{"type":"error","name":"SafeCastOverflowedUintDowncast","inputs":[{"name":"bits","type":"uint8","internalType":"uint8"},{"name":"value","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"SecondsPerSlotCannotBeZero","inputs":[]},{"type":"error","name":"SenderIsNotTheConsensusContract","inputs":[]},{"type":"error","name":"SenderNotAllowed","inputs":[]},{"type":"error","name":"UnexpectedChainConfig","inputs":[]},{"type":"error","name":"UnexpectedConsensusVersion","inputs":[{"name":"expectedVersion","type":"uint256","internalType":"uint256"},{"name":"receivedVersion","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"UnexpectedContractVersion","inputs":[{"name":"expected","type":"uint256","internalType":"uint256"},{"name":"received","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"UnexpectedDataHash","inputs":[{"name":"consensusHash","type":"bytes32","internalType":"bytes32"},{"name":"receivedHash","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"UnexpectedRefSlot","inputs":[{"name":"consensusRefSlot","type":"uint256","internalType":"uint256"},{"name":"dataRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"VersionCannotBeSame","inputs":[]},{"type":"error","name":"VersionCannotBeZero","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroFeeDistributorAddress","inputs":[]},{"type":"error","name":"ZeroPauseDuration","inputs":[]},{"type":"error","name":"ZeroStrikesAddress","inputs":[]}] +[{"type":"constructor","inputs":[{"name":"feeDistributor","type":"address","internalType":"address"},{"name":"strikes","type":"address","internalType":"address"},{"name":"secondsPerSlot","type":"uint256","internalType":"uint256"},{"name":"genesisTime","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"FEE_DISTRIBUTOR","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSFeeDistributor"}],"stateMutability":"view"},{"type":"function","name":"GENESIS_TIME","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"MANAGE_CONSENSUS_CONTRACT_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"MANAGE_CONSENSUS_VERSION_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_INFINITELY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RECOVERER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RESUME_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"SECONDS_PER_SLOT","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"STRIKES","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSStrikes"}],"stateMutability":"view"},{"type":"function","name":"SUBMIT_DATA_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"discardConsensusReport","inputs":[{"name":"refSlot","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"finalizeUpgradeV2","inputs":[{"name":"consensusVersion","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"getConsensusContract","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getConsensusReport","inputs":[],"outputs":[{"name":"hash","type":"bytes32","internalType":"bytes32"},{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"processingDeadlineTime","type":"uint256","internalType":"uint256"},{"name":"processingStarted","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"getConsensusVersion","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getContractVersion","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getLastProcessingRefSlot","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getResumeSinceTimestamp","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"admin","type":"address","internalType":"address"},{"name":"consensusContract","type":"address","internalType":"address"},{"name":"consensusVersion","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"isPaused","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"pauseFor","inputs":[{"name":"duration","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"pauseUntil","inputs":[{"name":"pauseUntilInclusive","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC1155","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC20","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC721","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverEther","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resume","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setConsensusContract","inputs":[{"name":"addr","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setConsensusVersion","inputs":[{"name":"version","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"submitConsensusReport","inputs":[{"name":"reportHash","type":"bytes32","internalType":"bytes32"},{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"submitReportData","inputs":[{"name":"data","type":"tuple","internalType":"struct ICSFeeOracle.ReportData","components":[{"name":"consensusVersion","type":"uint256","internalType":"uint256"},{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"treeRoot","type":"bytes32","internalType":"bytes32"},{"name":"treeCid","type":"string","internalType":"string"},{"name":"logCid","type":"string","internalType":"string"},{"name":"distributed","type":"uint256","internalType":"uint256"},{"name":"rebate","type":"uint256","internalType":"uint256"},{"name":"strikesTreeRoot","type":"bytes32","internalType":"bytes32"},{"name":"strikesTreeCid","type":"string","internalType":"string"}]},{"name":"contractVersion","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"event","name":"ConsensusHashContractSet","inputs":[{"name":"addr","type":"address","indexed":true,"internalType":"address"},{"name":"prevAddr","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"ConsensusVersionSet","inputs":[{"name":"version","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"prevVersion","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ContractVersionSet","inputs":[{"name":"version","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC1155Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC20Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC721Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"EtherRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"Paused","inputs":[{"name":"duration","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ProcessingStarted","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"hash","type":"bytes32","indexed":false,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"ReportDiscarded","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"hash","type":"bytes32","indexed":false,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"ReportSubmitted","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"hash","type":"bytes32","indexed":false,"internalType":"bytes32"},{"name":"processingDeadlineTime","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Resumed","inputs":[],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"StETHSharesRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"WarnProcessingMissed","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"AddressCannotBeSame","inputs":[]},{"type":"error","name":"AddressCannotBeZero","inputs":[]},{"type":"error","name":"FailedToSendEther","inputs":[]},{"type":"error","name":"HashCannotBeZero","inputs":[]},{"type":"error","name":"InitialRefSlotCannotBeLessThanProcessingOne","inputs":[{"name":"initialRefSlot","type":"uint256","internalType":"uint256"},{"name":"processingRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"InvalidContractVersion","inputs":[]},{"type":"error","name":"InvalidContractVersionIncrement","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"NoConsensusReportToProcess","inputs":[]},{"type":"error","name":"NonZeroContractVersionOnInit","inputs":[]},{"type":"error","name":"NotAllowedToRecover","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"PauseUntilMustBeInFuture","inputs":[]},{"type":"error","name":"PausedExpected","inputs":[]},{"type":"error","name":"ProcessingDeadlineMissed","inputs":[{"name":"deadline","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"RefSlotAlreadyProcessing","inputs":[]},{"type":"error","name":"RefSlotCannotDecrease","inputs":[{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"prevRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"RefSlotMustBeGreaterThanProcessingOne","inputs":[{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"processingRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"ResumedExpected","inputs":[]},{"type":"error","name":"SafeCastOverflowedUintDowncast","inputs":[{"name":"bits","type":"uint8","internalType":"uint8"},{"name":"value","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"SecondsPerSlotCannotBeZero","inputs":[]},{"type":"error","name":"SenderIsNotTheConsensusContract","inputs":[]},{"type":"error","name":"SenderNotAllowed","inputs":[]},{"type":"error","name":"UnexpectedChainConfig","inputs":[]},{"type":"error","name":"UnexpectedConsensusVersion","inputs":[{"name":"expectedVersion","type":"uint256","internalType":"uint256"},{"name":"receivedVersion","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"UnexpectedContractVersion","inputs":[{"name":"expected","type":"uint256","internalType":"uint256"},{"name":"received","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"UnexpectedDataHash","inputs":[{"name":"consensusHash","type":"bytes32","internalType":"bytes32"},{"name":"receivedHash","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"UnexpectedRefSlot","inputs":[{"name":"consensusRefSlot","type":"uint256","internalType":"uint256"},{"name":"dataRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"VersionCannotBeSame","inputs":[]},{"type":"error","name":"VersionCannotBeZero","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroFeeDistributorAddress","inputs":[]},{"type":"error","name":"ZeroPauseDuration","inputs":[]},{"type":"error","name":"ZeroStrikesAddress","inputs":[]}] \ No newline at end of file diff --git a/assets/CSModule.json b/assets/CSModule.json index 53518035f..22e10660e 100644 --- a/assets/CSModule.json +++ b/assets/CSModule.json @@ -1 +1 @@ -[{"type":"constructor","inputs":[{"name":"moduleType","type":"bytes32","internalType":"bytes32"},{"name":"lidoLocator","type":"address","internalType":"address"},{"name":"parametersRegistry","type":"address","internalType":"address"}],"stateMutability":"nonpayable"},{"type":"function","name":"BAD_PERFORMER_EJECTOR_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"CREATE_NODE_OPERATOR_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"LIDO_LOCATOR","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ILidoLocator"}],"stateMutability":"view"},{"type":"function","name":"PARAMETERS_REGISTRY","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSParametersRegistry"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_INFINITELY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RECOVERER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"REPORT_EL_REWARDS_STEALING_PENALTY_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RESUME_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"SETTLE_EL_REWARDS_STEALING_PENALTY_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"SET_BOND_CURVE_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"STAKING_ROUTER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"STETH","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IStETH"}],"stateMutability":"view"},{"type":"function","name":"VERIFIER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"accounting","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSAccounting"}],"stateMutability":"view"},{"type":"function","name":"addValidatorKeysETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"addValidatorKeysStETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"addValidatorKeysWstETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"cancelELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"changeNodeOperatorRewardAddress","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"newAddress","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"claimRewardsStETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stETHAmount","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"claimedShares","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"claimRewardsUnstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stEthAmount","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"requestId","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"claimRewardsWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"wstETHAmount","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"claimedWstETHAmount","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"cleanDepositQueue","inputs":[{"name":"maxItems","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"removed","type":"uint256","internalType":"uint256"},{"name":"lastRemovedAtDepth","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"compensateELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"confirmNodeOperatorManagerAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"confirmNodeOperatorRewardAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"createNodeOperator","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"managementProperties","type":"tuple","internalType":"struct NodeOperatorManagementProperties","components":[{"name":"managerAddress","type":"address","internalType":"address"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"extendedManagerPermissions","type":"bool","internalType":"bool"}]},{"name":"referrer","type":"address","internalType":"address"}],"outputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"decreaseVettedSigningKeysCount","inputs":[{"name":"nodeOperatorIds","type":"bytes","internalType":"bytes"},{"name":"vettedSigningKeysCounts","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"depositETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"depositQueue","inputs":[],"outputs":[{"name":"head","type":"uint128","internalType":"uint128"},{"name":"tail","type":"uint128","internalType":"uint128"}],"stateMutability":"view"},{"type":"function","name":"depositQueueItem","inputs":[{"name":"index","type":"uint128","internalType":"uint128"}],"outputs":[{"name":"","type":"uint256","internalType":"Batch"}],"stateMutability":"view"},{"type":"function","name":"depositStETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stETHAmount","type":"uint256","internalType":"uint256"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"depositWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"wstETHAmount","type":"uint256","internalType":"uint256"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"ejectBadPerformer","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"},{"name":"strikesCount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"enqueueNodeOperatorKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"finalizeUpgradeV2","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"getActiveNodeOperatorsCount","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperator","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct NodeOperator","components":[{"name":"totalAddedKeys","type":"uint32","internalType":"uint32"},{"name":"totalWithdrawnKeys","type":"uint32","internalType":"uint32"},{"name":"totalDepositedKeys","type":"uint32","internalType":"uint32"},{"name":"totalVettedKeys","type":"uint32","internalType":"uint32"},{"name":"stuckValidatorsCount","type":"uint32","internalType":"uint32"},{"name":"depositableValidatorsCount","type":"uint32","internalType":"uint32"},{"name":"targetLimit","type":"uint32","internalType":"uint32"},{"name":"targetLimitMode","type":"uint8","internalType":"uint8"},{"name":"totalExitedKeys","type":"uint32","internalType":"uint32"},{"name":"enqueuedCount","type":"uint32","internalType":"uint32"},{"name":"managerAddress","type":"address","internalType":"address"},{"name":"proposedManagerAddress","type":"address","internalType":"address"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"proposedRewardAddress","type":"address","internalType":"address"},{"name":"extendedManagerPermissions","type":"bool","internalType":"bool"}]}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorIds","inputs":[{"name":"offset","type":"uint256","internalType":"uint256"},{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"nodeOperatorIds","type":"uint256[]","internalType":"uint256[]"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorIsActive","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorNonWithdrawnKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorSummary","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"targetLimitMode","type":"uint256","internalType":"uint256"},{"name":"targetValidatorsCount","type":"uint256","internalType":"uint256"},{"name":"stuckValidatorsCount","type":"uint256","internalType":"uint256"},{"name":"refundedValidatorsCount","type":"uint256","internalType":"uint256"},{"name":"stuckPenaltyEndTimestamp","type":"uint256","internalType":"uint256"},{"name":"totalExitedValidators","type":"uint256","internalType":"uint256"},{"name":"totalDepositedValidators","type":"uint256","internalType":"uint256"},{"name":"depositableValidatorsCount","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorsCount","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNonce","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getResumeSinceTimestamp","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getSigningKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"startIndex","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bytes","internalType":"bytes"}],"stateMutability":"view"},{"type":"function","name":"getSigningKeysWithSignatures","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"startIndex","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"keys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"}],"stateMutability":"view"},{"type":"function","name":"getStakingModuleSummary","inputs":[],"outputs":[{"name":"totalExitedValidators","type":"uint256","internalType":"uint256"},{"name":"totalDepositedValidators","type":"uint256","internalType":"uint256"},{"name":"depositableValidatorsCount","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getType","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"_accounting","type":"address","internalType":"address"},{"name":"admin","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"isPaused","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"isValidatorEjected","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"isValidatorWithdrawn","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"obtainDepositData","inputs":[{"name":"depositsCount","type":"uint256","internalType":"uint256"},{"name":"","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"}],"stateMutability":"nonpayable"},{"type":"function","name":"onExitedAndStuckValidatorsCountsUpdated","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"onRewardsMinted","inputs":[{"name":"totalShares","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"onWithdrawalCredentialsChanged","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"pauseFor","inputs":[{"name":"duration","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"proposeNodeOperatorManagerAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"proposedAddress","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"proposeNodeOperatorRewardAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"proposedAddress","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"publicRelease","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"recoverERC1155","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC20","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC721","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverEther","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"removeKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"startIndex","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"reportELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"blockHash","type":"bytes32","internalType":"bytes32"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resetNodeOperatorManagerAddress","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resume","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setBondCurve","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"settleELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorIds","type":"uint256[]","internalType":"uint256[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"submitWithdrawal","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"},{"name":"isSlashed","type":"bool","internalType":"bool"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"unsafeUpdateValidatorsCount","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"exitedValidatorsKeysCount","type":"uint256","internalType":"uint256"},{"name":"stuckValidatorsKeysCount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateExitedValidatorsCount","inputs":[{"name":"nodeOperatorIds","type":"bytes","internalType":"bytes"},{"name":"exitedValidatorsCounts","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateRefundedValidatorsCount","inputs":[{"name":"","type":"uint256","internalType":"uint256"},{"name":"","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"view"},{"type":"function","name":"updateStuckValidatorsCount","inputs":[{"name":"nodeOperatorIds","type":"bytes","internalType":"bytes"},{"name":"stuckValidatorsCounts","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateTargetValidatorsLimits","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"targetLimitMode","type":"uint256","internalType":"uint256"},{"name":"targetLimit","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"BatchEnqueued","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"count","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DepositableSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"depositableKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DepositedSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"depositedKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltyCancelled","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltyCompensated","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltyReported","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"proposedBlockHash","type":"bytes32","indexed":false,"internalType":"bytes32"},{"name":"stolenAmount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltySettled","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC1155Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC20Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC721Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"EjectionSubmitted","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"keyIndex","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"EtherRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ExitedSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"exitedKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"KeyRemovalChargeApplied","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"NodeOperatorAdded","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"managerAddress","type":"address","indexed":true,"internalType":"address"},{"name":"rewardAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NodeOperatorManagerAddressChangeProposed","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"oldProposedAddress","type":"address","indexed":true,"internalType":"address"},{"name":"newProposedAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NodeOperatorManagerAddressChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"oldAddress","type":"address","indexed":true,"internalType":"address"},{"name":"newAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NodeOperatorRewardAddressChangeProposed","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"oldProposedAddress","type":"address","indexed":true,"internalType":"address"},{"name":"newProposedAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NodeOperatorRewardAddressChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"oldAddress","type":"address","indexed":true,"internalType":"address"},{"name":"newAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NonceChanged","inputs":[{"name":"nonce","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Paused","inputs":[{"name":"duration","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ReferrerSet","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"referrer","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"Resumed","inputs":[],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"SigningKeyAdded","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"SigningKeyRemoved","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"StETHSharesRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"StuckSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"stuckKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"TargetValidatorsCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"targetLimitMode","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"targetValidatorsCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"TotalSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"totalKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"VettedSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"vettedKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"VettedSigningKeysCountDecreased","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"WithdrawalSubmitted","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"keyIndex","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"AlreadyEjected","inputs":[]},{"type":"error","name":"AlreadyProposed","inputs":[]},{"type":"error","name":"AlreadyWithdrawn","inputs":[]},{"type":"error","name":"EmptyKey","inputs":[]},{"type":"error","name":"ExitedKeysDecrease","inputs":[]},{"type":"error","name":"ExitedKeysHigherThanTotalDeposited","inputs":[]},{"type":"error","name":"FailedToSendEther","inputs":[]},{"type":"error","name":"InvalidAmount","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidInput","inputs":[]},{"type":"error","name":"InvalidKeysCount","inputs":[]},{"type":"error","name":"InvalidLength","inputs":[]},{"type":"error","name":"InvalidReportData","inputs":[]},{"type":"error","name":"InvalidVetKeysPointer","inputs":[]},{"type":"error","name":"MethodCallIsNotAllowed","inputs":[]},{"type":"error","name":"NodeOperatorDoesNotExist","inputs":[]},{"type":"error","name":"NodeOperatorHasKeys","inputs":[]},{"type":"error","name":"NotAllowedToRecover","inputs":[]},{"type":"error","name":"NotEnoughKeys","inputs":[]},{"type":"error","name":"NotEnoughStrikesToEject","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"NotSupported","inputs":[]},{"type":"error","name":"PauseUntilMustBeInFuture","inputs":[]},{"type":"error","name":"PausedExpected","inputs":[]},{"type":"error","name":"QueueIsEmpty","inputs":[]},{"type":"error","name":"QueueLookupNoLimit","inputs":[]},{"type":"error","name":"ResumedExpected","inputs":[]},{"type":"error","name":"SameAddress","inputs":[]},{"type":"error","name":"SenderIsNotEligible","inputs":[]},{"type":"error","name":"SenderIsNotManagerAddress","inputs":[]},{"type":"error","name":"SenderIsNotProposedAddress","inputs":[]},{"type":"error","name":"SenderIsNotRewardAddress","inputs":[]},{"type":"error","name":"SigningKeysInvalidOffset","inputs":[]},{"type":"error","name":"StuckKeysHigherThanNonExited","inputs":[]},{"type":"error","name":"ZeroAccountingAddress","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroLocatorAddress","inputs":[]},{"type":"error","name":"ZeroParametersRegistryAddress","inputs":[]},{"type":"error","name":"ZeroPauseDuration","inputs":[]},{"type":"error","name":"ZeroRewardAddress","inputs":[]},{"type":"error","name":"ZeroSenderAddress","inputs":[]}] \ No newline at end of file +[{"type":"constructor","inputs":[{"name":"moduleType","type":"bytes32","internalType":"bytes32"},{"name":"lidoLocator","type":"address","internalType":"address"},{"name":"parametersRegistry","type":"address","internalType":"address"},{"name":"_accounting","type":"address","internalType":"address"},{"name":"exitPenalties","type":"address","internalType":"address"}],"stateMutability":"nonpayable"},{"type":"function","name":"ACCOUNTING","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSAccounting"}],"stateMutability":"view"},{"type":"function","name":"CREATE_NODE_OPERATOR_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"DEPOSIT_SIZE","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"EXIT_PENALTIES","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSExitPenalties"}],"stateMutability":"view"},{"type":"function","name":"LIDO_LOCATOR","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ILidoLocator"}],"stateMutability":"view"},{"type":"function","name":"PARAMETERS_REGISTRY","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSParametersRegistry"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_INFINITELY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"QUEUE_LEGACY_PRIORITY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"QUEUE_LOWEST_PRIORITY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"RECOVERER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"REPORT_EL_REWARDS_STEALING_PENALTY_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RESUME_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"SETTLE_EL_REWARDS_STEALING_PENALTY_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"STAKING_ROUTER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"STETH","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IStETH"}],"stateMutability":"view"},{"type":"function","name":"VERIFIER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"accounting","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSAccounting"}],"stateMutability":"view"},{"type":"function","name":"addValidatorKeysETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"addValidatorKeysStETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"addValidatorKeysWstETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"cancelELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"changeNodeOperatorRewardAddress","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"newAddress","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"cleanDepositQueue","inputs":[{"name":"maxItems","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"removed","type":"uint256","internalType":"uint256"},{"name":"lastRemovedAtDepth","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"compensateELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"confirmNodeOperatorManagerAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"confirmNodeOperatorRewardAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"createNodeOperator","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"managementProperties","type":"tuple","internalType":"struct NodeOperatorManagementProperties","components":[{"name":"managerAddress","type":"address","internalType":"address"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"extendedManagerPermissions","type":"bool","internalType":"bool"}]},{"name":"referrer","type":"address","internalType":"address"}],"outputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"decreaseVettedSigningKeysCount","inputs":[{"name":"nodeOperatorIds","type":"bytes","internalType":"bytes"},{"name":"vettedSigningKeysCounts","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"depositQueueItem","inputs":[{"name":"queuePriority","type":"uint256","internalType":"uint256"},{"name":"index","type":"uint128","internalType":"uint128"}],"outputs":[{"name":"","type":"uint256","internalType":"Batch"}],"stateMutability":"view"},{"type":"function","name":"depositQueuePointers","inputs":[{"name":"queuePriority","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"head","type":"uint128","internalType":"uint128"},{"name":"tail","type":"uint128","internalType":"uint128"}],"stateMutability":"view"},{"type":"function","name":"exitDeadlineThreshold","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"finalizeUpgradeV2","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"getActiveNodeOperatorsCount","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getInitializedVersion","inputs":[],"outputs":[{"name":"","type":"uint64","internalType":"uint64"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperator","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct NodeOperator","components":[{"name":"totalAddedKeys","type":"uint32","internalType":"uint32"},{"name":"totalWithdrawnKeys","type":"uint32","internalType":"uint32"},{"name":"totalDepositedKeys","type":"uint32","internalType":"uint32"},{"name":"totalVettedKeys","type":"uint32","internalType":"uint32"},{"name":"stuckValidatorsCount","type":"uint32","internalType":"uint32"},{"name":"depositableValidatorsCount","type":"uint32","internalType":"uint32"},{"name":"targetLimit","type":"uint32","internalType":"uint32"},{"name":"targetLimitMode","type":"uint8","internalType":"uint8"},{"name":"totalExitedKeys","type":"uint32","internalType":"uint32"},{"name":"enqueuedCount","type":"uint32","internalType":"uint32"},{"name":"managerAddress","type":"address","internalType":"address"},{"name":"proposedManagerAddress","type":"address","internalType":"address"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"proposedRewardAddress","type":"address","internalType":"address"},{"name":"extendedManagerPermissions","type":"bool","internalType":"bool"},{"name":"usedPriorityQueue","type":"bool","internalType":"bool"}]}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorIds","inputs":[{"name":"offset","type":"uint256","internalType":"uint256"},{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"nodeOperatorIds","type":"uint256[]","internalType":"uint256[]"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorIsActive","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorManagementProperties","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct NodeOperatorManagementProperties","components":[{"name":"managerAddress","type":"address","internalType":"address"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"extendedManagerPermissions","type":"bool","internalType":"bool"}]}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorNonWithdrawnKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorSummary","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"targetLimitMode","type":"uint256","internalType":"uint256"},{"name":"targetValidatorsCount","type":"uint256","internalType":"uint256"},{"name":"stuckValidatorsCount","type":"uint256","internalType":"uint256"},{"name":"refundedValidatorsCount","type":"uint256","internalType":"uint256"},{"name":"stuckPenaltyEndTimestamp","type":"uint256","internalType":"uint256"},{"name":"totalExitedValidators","type":"uint256","internalType":"uint256"},{"name":"totalDepositedValidators","type":"uint256","internalType":"uint256"},{"name":"depositableValidatorsCount","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorTotalDepositedKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"totalDepositedKeys","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorsCount","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNonce","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getResumeSinceTimestamp","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getSigningKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"startIndex","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bytes","internalType":"bytes"}],"stateMutability":"view"},{"type":"function","name":"getSigningKeysWithSignatures","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"startIndex","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"keys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"}],"stateMutability":"view"},{"type":"function","name":"getStakingModuleSummary","inputs":[],"outputs":[{"name":"totalExitedValidators","type":"uint256","internalType":"uint256"},{"name":"totalDepositedValidators","type":"uint256","internalType":"uint256"},{"name":"depositableValidatorsCount","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getType","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"admin","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"isPaused","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"isValidatorExitDelayPenaltyApplicable","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"","type":"uint256","internalType":"uint256"},{"name":"publicKey","type":"bytes","internalType":"bytes"},{"name":"eligibleToExitInSec","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"isValidatorWithdrawn","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"legacyQueue","inputs":[],"outputs":[{"name":"head","type":"uint128","internalType":"uint128"},{"name":"tail","type":"uint128","internalType":"uint128"}],"stateMutability":"view"},{"type":"function","name":"migrateToPriorityQueue","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"obtainDepositData","inputs":[{"name":"depositsCount","type":"uint256","internalType":"uint256"},{"name":"","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"}],"stateMutability":"nonpayable"},{"type":"function","name":"onExitedAndStuckValidatorsCountsUpdated","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"onRewardsMinted","inputs":[{"name":"totalShares","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"onValidatorExitTriggered","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"publicKey","type":"bytes","internalType":"bytes"},{"name":"withdrawalRequestPaidFee","type":"uint256","internalType":"uint256"},{"name":"exitType","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"onWithdrawalCredentialsChanged","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"pauseFor","inputs":[{"name":"duration","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"proposeNodeOperatorManagerAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"proposedAddress","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"proposeNodeOperatorRewardAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"proposedAddress","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC1155","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC20","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC721","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverEther","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"removeKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"startIndex","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"reportELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"blockHash","type":"bytes32","internalType":"bytes32"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"reportValidatorExitDelay","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"","type":"uint256","internalType":"uint256"},{"name":"publicKey","type":"bytes","internalType":"bytes"},{"name":"eligibleToExitInSec","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resetNodeOperatorManagerAddress","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resume","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"settleELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorIds","type":"uint256[]","internalType":"uint256[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"submitWithdrawals","inputs":[{"name":"withdrawalsInfo","type":"tuple[]","internalType":"struct ValidatorWithdrawalInfo[]","components":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"},{"name":"isSlashed","type":"bool","internalType":"bool"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"unsafeUpdateValidatorsCount","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"exitedValidatorsKeysCount","type":"uint256","internalType":"uint256"},{"name":"stuckValidatorsKeysCount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateDepositableValidatorsCount","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateExitedValidatorsCount","inputs":[{"name":"nodeOperatorIds","type":"bytes","internalType":"bytes"},{"name":"exitedValidatorsCounts","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateTargetValidatorsLimits","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"targetLimitMode","type":"uint256","internalType":"uint256"},{"name":"targetLimit","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"BatchEnqueued","inputs":[{"name":"queuePriority","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"count","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DelayedValidatorExitPenalized","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"penaltyValue","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DepositableSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"depositableKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DepositedSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"depositedKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltyCancelled","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltyCompensated","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltyReported","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"proposedBlockHash","type":"bytes32","indexed":false,"internalType":"bytes32"},{"name":"stolenAmount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltySettled","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC1155Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC20Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC721Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"EtherRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ExitedSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"exitedKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"KeyRemovalChargeApplied","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"NodeOperatorAdded","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"managerAddress","type":"address","indexed":true,"internalType":"address"},{"name":"rewardAddress","type":"address","indexed":true,"internalType":"address"},{"name":"extendedManagerPermissions","type":"bool","indexed":false,"internalType":"bool"}],"anonymous":false},{"type":"event","name":"NodeOperatorManagerAddressChangeProposed","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"oldProposedAddress","type":"address","indexed":true,"internalType":"address"},{"name":"newProposedAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NodeOperatorManagerAddressChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"oldAddress","type":"address","indexed":true,"internalType":"address"},{"name":"newAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NodeOperatorRewardAddressChangeProposed","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"oldProposedAddress","type":"address","indexed":true,"internalType":"address"},{"name":"newProposedAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NodeOperatorRewardAddressChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"oldAddress","type":"address","indexed":true,"internalType":"address"},{"name":"newAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NonceChanged","inputs":[{"name":"nonce","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Paused","inputs":[{"name":"duration","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ReferrerSet","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"referrer","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"Resumed","inputs":[],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"SigningKeyAdded","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"SigningKeyRemoved","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"StETHSharesRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"TargetValidatorsCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"targetLimitMode","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"targetValidatorsCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"TotalSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"totalKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"VettedSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"vettedKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"VettedSigningKeysCountDecreased","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"WithdrawalSubmitted","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"keyIndex","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"AlreadyProposed","inputs":[]},{"type":"error","name":"AlreadyWithdrawn","inputs":[]},{"type":"error","name":"CannotAddKeys","inputs":[]},{"type":"error","name":"EmptyKey","inputs":[]},{"type":"error","name":"ExitedKeysDecrease","inputs":[]},{"type":"error","name":"ExitedKeysHigherThanTotalDeposited","inputs":[]},{"type":"error","name":"FailedToSendEther","inputs":[]},{"type":"error","name":"InvalidAmount","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidInput","inputs":[]},{"type":"error","name":"InvalidKeysCount","inputs":[]},{"type":"error","name":"InvalidLength","inputs":[]},{"type":"error","name":"InvalidReportData","inputs":[]},{"type":"error","name":"InvalidVetKeysPointer","inputs":[]},{"type":"error","name":"KeysLimitExceeded","inputs":[]},{"type":"error","name":"MethodCallIsNotAllowed","inputs":[]},{"type":"error","name":"NodeOperatorDoesNotExist","inputs":[]},{"type":"error","name":"NotAllowedToRecover","inputs":[]},{"type":"error","name":"NotEnoughKeys","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"PauseUntilMustBeInFuture","inputs":[]},{"type":"error","name":"PausedExpected","inputs":[]},{"type":"error","name":"PriorityQueueAlreadyUsed","inputs":[]},{"type":"error","name":"QueueIsEmpty","inputs":[]},{"type":"error","name":"QueueLookupNoLimit","inputs":[]},{"type":"error","name":"ResumedExpected","inputs":[]},{"type":"error","name":"SameAddress","inputs":[]},{"type":"error","name":"SenderIsNotEligible","inputs":[]},{"type":"error","name":"SenderIsNotManagerAddress","inputs":[]},{"type":"error","name":"SenderIsNotProposedAddress","inputs":[]},{"type":"error","name":"SenderIsNotRewardAddress","inputs":[]},{"type":"error","name":"SigningKeysInvalidOffset","inputs":[]},{"type":"error","name":"ZeroAccountingAddress","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroExitPenaltiesAddress","inputs":[]},{"type":"error","name":"ZeroLocatorAddress","inputs":[]},{"type":"error","name":"ZeroParametersRegistryAddress","inputs":[]},{"type":"error","name":"ZeroPauseDuration","inputs":[]},{"type":"error","name":"ZeroRewardAddress","inputs":[]},{"type":"error","name":"ZeroSenderAddress","inputs":[]}] \ No newline at end of file diff --git a/assets/CSParametersRegistry.json b/assets/CSParametersRegistry.json index 0abea6d1e..325cb76ce 100644 --- a/assets/CSParametersRegistry.json +++ b/assets/CSParametersRegistry.json @@ -1 +1 @@ -[{"type":"constructor","inputs":[{"name":"queueLowestPriority","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"QUEUE_LEGACY_PRIORITY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"QUEUE_LOWEST_PRIORITY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultAllowedExitDelay","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultBadPerformancePenalty","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultElRewardsStealingAdditionalFine","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultExitDelayPenalty","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultKeyRemovalCharge","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultKeysLimit","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultMaxWithdrawalRequestFee","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultPerformanceCoefficients","inputs":[],"outputs":[{"name":"attestationsWeight","type":"uint32","internalType":"uint32"},{"name":"blocksWeight","type":"uint32","internalType":"uint32"},{"name":"syncWeight","type":"uint32","internalType":"uint32"}],"stateMutability":"view"},{"type":"function","name":"defaultPerformanceLeeway","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultQueueConfig","inputs":[],"outputs":[{"name":"priority","type":"uint32","internalType":"uint32"},{"name":"maxDeposits","type":"uint32","internalType":"uint32"}],"stateMutability":"view"},{"type":"function","name":"defaultRewardShare","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultStrikesParams","inputs":[],"outputs":[{"name":"lifetime","type":"uint32","internalType":"uint32"},{"name":"threshold","type":"uint32","internalType":"uint32"}],"stateMutability":"view"},{"type":"function","name":"getAllowedExitDelay","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"delay","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBadPerformancePenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"penalty","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getElRewardsStealingAdditionalFine","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"fine","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getExitDelayPenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"penalty","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getKeyRemovalCharge","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"keyRemovalCharge","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getKeysLimit","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"limit","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getMaxWithdrawalRequestFee","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"fee","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getPerformanceCoefficients","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"attestationsWeight","type":"uint256","internalType":"uint256"},{"name":"blocksWeight","type":"uint256","internalType":"uint256"},{"name":"syncWeight","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getPerformanceLeewayData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"data","type":"tuple[]","internalType":"struct ICSParametersRegistry.KeyIndexValueInterval[]","components":[{"name":"minKeyIndex","type":"uint256","internalType":"uint256"},{"name":"value","type":"uint256","internalType":"uint256"}]}],"stateMutability":"view"},{"type":"function","name":"getQueueConfig","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"queuePriority","type":"uint32","internalType":"uint32"},{"name":"maxDeposits","type":"uint32","internalType":"uint32"}],"stateMutability":"view"},{"type":"function","name":"getRewardShareData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"data","type":"tuple[]","internalType":"struct ICSParametersRegistry.KeyIndexValueInterval[]","components":[{"name":"minKeyIndex","type":"uint256","internalType":"uint256"},{"name":"value","type":"uint256","internalType":"uint256"}]}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getStrikesParams","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"lifetime","type":"uint256","internalType":"uint256"},{"name":"threshold","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"admin","type":"address","internalType":"address"},{"name":"data","type":"tuple","internalType":"struct ICSParametersRegistry.InitializationData","components":[{"name":"keyRemovalCharge","type":"uint256","internalType":"uint256"},{"name":"elRewardsStealingAdditionalFine","type":"uint256","internalType":"uint256"},{"name":"keysLimit","type":"uint256","internalType":"uint256"},{"name":"rewardShare","type":"uint256","internalType":"uint256"},{"name":"performanceLeeway","type":"uint256","internalType":"uint256"},{"name":"strikesLifetime","type":"uint256","internalType":"uint256"},{"name":"strikesThreshold","type":"uint256","internalType":"uint256"},{"name":"defaultQueuePriority","type":"uint256","internalType":"uint256"},{"name":"defaultQueueMaxDeposits","type":"uint256","internalType":"uint256"},{"name":"badPerformancePenalty","type":"uint256","internalType":"uint256"},{"name":"attestationsWeight","type":"uint256","internalType":"uint256"},{"name":"blocksWeight","type":"uint256","internalType":"uint256"},{"name":"syncWeight","type":"uint256","internalType":"uint256"},{"name":"defaultAllowedExitDelay","type":"uint256","internalType":"uint256"},{"name":"defaultExitDelayPenalty","type":"uint256","internalType":"uint256"},{"name":"defaultMaxWithdrawalRequestFee","type":"uint256","internalType":"uint256"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setAllowedExitDelay","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"delay","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setBadPerformancePenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"penalty","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultAllowedExitDelay","inputs":[{"name":"delay","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultBadPerformancePenalty","inputs":[{"name":"penalty","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultElRewardsStealingAdditionalFine","inputs":[{"name":"fine","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultExitDelayPenalty","inputs":[{"name":"penalty","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultKeyRemovalCharge","inputs":[{"name":"keyRemovalCharge","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultKeysLimit","inputs":[{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultMaxWithdrawalRequestFee","inputs":[{"name":"fee","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultPerformanceCoefficients","inputs":[{"name":"attestationsWeight","type":"uint256","internalType":"uint256"},{"name":"blocksWeight","type":"uint256","internalType":"uint256"},{"name":"syncWeight","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultPerformanceLeeway","inputs":[{"name":"leeway","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultQueueConfig","inputs":[{"name":"priority","type":"uint256","internalType":"uint256"},{"name":"maxDeposits","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultRewardShare","inputs":[{"name":"share","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultStrikesParams","inputs":[{"name":"lifetime","type":"uint256","internalType":"uint256"},{"name":"threshold","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setElRewardsStealingAdditionalFine","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"fine","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setExitDelayPenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"penalty","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setKeyRemovalCharge","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"keyRemovalCharge","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setKeysLimit","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setMaxWithdrawalRequestFee","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"fee","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setPerformanceCoefficients","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"attestationsWeight","type":"uint256","internalType":"uint256"},{"name":"blocksWeight","type":"uint256","internalType":"uint256"},{"name":"syncWeight","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setPerformanceLeewayData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"data","type":"uint256[2][]","internalType":"uint256[2][]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setQueueConfig","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"priority","type":"uint32","internalType":"uint32"},{"name":"maxDeposits","type":"uint32","internalType":"uint32"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setRewardShareData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"data","type":"uint256[2][]","internalType":"uint256[2][]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setStrikesParams","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"lifetime","type":"uint256","internalType":"uint256"},{"name":"threshold","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"unsetAllowedExitDelay","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetBadPerformancePenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetElRewardsStealingAdditionalFine","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetExitDelayPenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetKeyRemovalCharge","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetKeysLimit","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetMaxWithdrawalRequestFee","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetPerformanceCoefficients","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetPerformanceLeewayData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetQueueConfig","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetRewardShareData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetStrikesParams","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"AllowedExitDelaySet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"delay","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"AllowedExitDelayUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BadPerformancePenaltySet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"penalty","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BadPerformancePenaltyUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultAllowedExitDelaySet","inputs":[{"name":"delay","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultBadPerformancePenaltySet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultElRewardsStealingAdditionalFineSet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultExitDelayPenaltySet","inputs":[{"name":"penalty","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultKeyRemovalChargeSet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultKeysLimitSet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultMaxWithdrawalRequestFeeSet","inputs":[{"name":"fee","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultPerformanceCoefficientsSet","inputs":[{"name":"attestationsWeight","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"blocksWeight","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"syncWeight","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultPerformanceLeewaySet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultQueueConfigSet","inputs":[{"name":"priority","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"maxDeposits","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultRewardShareSet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultStrikesParamsSet","inputs":[{"name":"lifetime","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"threshold","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ElRewardsStealingAdditionalFineSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"fine","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ElRewardsStealingAdditionalFineUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ExitDelayPenaltySet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"penalty","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ExitDelayPenaltyUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"KeyRemovalChargeSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"keyRemovalCharge","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"KeyRemovalChargeUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"KeysLimitSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"limit","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"KeysLimitUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"MaxWithdrawalRequestFeeSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"fee","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"MaxWithdrawalRequestFeeUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PerformanceCoefficientsSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"attestationsWeight","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"blocksWeight","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"syncWeight","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PerformanceCoefficientsUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PerformanceLeewayDataSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"data","type":"uint256[2][]","indexed":false,"internalType":"uint256[2][]"}],"anonymous":false},{"type":"event","name":"PerformanceLeewayDataUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"QueueConfigSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"priority","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"maxDeposits","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"QueueConfigUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"RewardShareDataSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"data","type":"uint256[2][]","indexed":false,"internalType":"uint256[2][]"}],"anonymous":false},{"type":"event","name":"RewardShareDataUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"StrikesParamsSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"lifetime","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"threshold","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"StrikesParamsUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"InvalidExitDelayPenalty","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidKeyIndexValueIntervals","inputs":[]},{"type":"error","name":"InvalidPerformanceCoefficients","inputs":[]},{"type":"error","name":"InvalidPerformanceLeewayData","inputs":[]},{"type":"error","name":"InvalidRewardShareData","inputs":[]},{"type":"error","name":"InvalidStrikesParams","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"QueueCannotBeUsed","inputs":[]},{"type":"error","name":"SafeCastOverflowedUintDowncast","inputs":[{"name":"bits","type":"uint8","internalType":"uint8"},{"name":"value","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroMaxDeposits","inputs":[]}] \ No newline at end of file +[{"type":"constructor","inputs":[{"name":"queueLowestPriority","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"QUEUE_LEGACY_PRIORITY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"QUEUE_LOWEST_PRIORITY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultAllowedExitDelay","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultBadPerformancePenalty","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultElRewardsStealingAdditionalFine","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultExitDelayPenalty","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultKeyRemovalCharge","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultKeysLimit","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultMaxWithdrawalRequestFee","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultPerformanceCoefficients","inputs":[],"outputs":[{"name":"attestationsWeight","type":"uint32","internalType":"uint32"},{"name":"blocksWeight","type":"uint32","internalType":"uint32"},{"name":"syncWeight","type":"uint32","internalType":"uint32"}],"stateMutability":"view"},{"type":"function","name":"defaultPerformanceLeeway","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultQueueConfig","inputs":[],"outputs":[{"name":"priority","type":"uint32","internalType":"uint32"},{"name":"maxDeposits","type":"uint32","internalType":"uint32"}],"stateMutability":"view"},{"type":"function","name":"defaultRewardShare","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultStrikesParams","inputs":[],"outputs":[{"name":"lifetime","type":"uint32","internalType":"uint32"},{"name":"threshold","type":"uint32","internalType":"uint32"}],"stateMutability":"view"},{"type":"function","name":"getAllowedExitDelay","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"delay","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBadPerformancePenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"penalty","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getElRewardsStealingAdditionalFine","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"fine","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getExitDelayPenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"penalty","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getInitializedVersion","inputs":[],"outputs":[{"name":"","type":"uint64","internalType":"uint64"}],"stateMutability":"view"},{"type":"function","name":"getKeyRemovalCharge","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"keyRemovalCharge","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getKeysLimit","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"limit","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getMaxWithdrawalRequestFee","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"fee","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getPerformanceCoefficients","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"attestationsWeight","type":"uint256","internalType":"uint256"},{"name":"blocksWeight","type":"uint256","internalType":"uint256"},{"name":"syncWeight","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getPerformanceLeewayData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"data","type":"tuple","internalType":"struct ICSParametersRegistry.KeyNumberValue","components":[{"name":"intervals","type":"tuple[]","internalType":"struct ICSParametersRegistry.KeyNumberValueInterval[]","components":[{"name":"minKeyNumber","type":"uint256","internalType":"uint256"},{"name":"value","type":"uint256","internalType":"uint256"}]}]}],"stateMutability":"view"},{"type":"function","name":"getQueueConfig","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"queuePriority","type":"uint32","internalType":"uint32"},{"name":"maxDeposits","type":"uint32","internalType":"uint32"}],"stateMutability":"view"},{"type":"function","name":"getRewardShareData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"data","type":"tuple","internalType":"struct ICSParametersRegistry.KeyNumberValue","components":[{"name":"intervals","type":"tuple[]","internalType":"struct ICSParametersRegistry.KeyNumberValueInterval[]","components":[{"name":"minKeyNumber","type":"uint256","internalType":"uint256"},{"name":"value","type":"uint256","internalType":"uint256"}]}]}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getStrikesParams","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"lifetime","type":"uint256","internalType":"uint256"},{"name":"threshold","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"admin","type":"address","internalType":"address"},{"name":"data","type":"tuple","internalType":"struct ICSParametersRegistry.InitializationData","components":[{"name":"keyRemovalCharge","type":"uint256","internalType":"uint256"},{"name":"elRewardsStealingAdditionalFine","type":"uint256","internalType":"uint256"},{"name":"keysLimit","type":"uint256","internalType":"uint256"},{"name":"rewardShare","type":"uint256","internalType":"uint256"},{"name":"performanceLeeway","type":"uint256","internalType":"uint256"},{"name":"strikesLifetime","type":"uint256","internalType":"uint256"},{"name":"strikesThreshold","type":"uint256","internalType":"uint256"},{"name":"defaultQueuePriority","type":"uint256","internalType":"uint256"},{"name":"defaultQueueMaxDeposits","type":"uint256","internalType":"uint256"},{"name":"badPerformancePenalty","type":"uint256","internalType":"uint256"},{"name":"attestationsWeight","type":"uint256","internalType":"uint256"},{"name":"blocksWeight","type":"uint256","internalType":"uint256"},{"name":"syncWeight","type":"uint256","internalType":"uint256"},{"name":"defaultAllowedExitDelay","type":"uint256","internalType":"uint256"},{"name":"defaultExitDelayPenalty","type":"uint256","internalType":"uint256"},{"name":"defaultMaxWithdrawalRequestFee","type":"uint256","internalType":"uint256"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setAllowedExitDelay","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"delay","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setBadPerformancePenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"penalty","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultAllowedExitDelay","inputs":[{"name":"delay","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultBadPerformancePenalty","inputs":[{"name":"penalty","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultElRewardsStealingAdditionalFine","inputs":[{"name":"fine","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultExitDelayPenalty","inputs":[{"name":"penalty","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultKeyRemovalCharge","inputs":[{"name":"keyRemovalCharge","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultKeysLimit","inputs":[{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultMaxWithdrawalRequestFee","inputs":[{"name":"fee","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultPerformanceCoefficients","inputs":[{"name":"attestationsWeight","type":"uint256","internalType":"uint256"},{"name":"blocksWeight","type":"uint256","internalType":"uint256"},{"name":"syncWeight","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultPerformanceLeeway","inputs":[{"name":"leeway","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultQueueConfig","inputs":[{"name":"priority","type":"uint256","internalType":"uint256"},{"name":"maxDeposits","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultRewardShare","inputs":[{"name":"share","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultStrikesParams","inputs":[{"name":"lifetime","type":"uint256","internalType":"uint256"},{"name":"threshold","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setElRewardsStealingAdditionalFine","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"fine","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setExitDelayPenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"penalty","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setKeyRemovalCharge","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"keyRemovalCharge","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setKeysLimit","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setMaxWithdrawalRequestFee","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"fee","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setPerformanceCoefficients","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"attestationsWeight","type":"uint256","internalType":"uint256"},{"name":"blocksWeight","type":"uint256","internalType":"uint256"},{"name":"syncWeight","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setPerformanceLeewayData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"data","type":"tuple[]","internalType":"struct ICSParametersRegistry.KeyNumberValueInterval[]","components":[{"name":"minKeyNumber","type":"uint256","internalType":"uint256"},{"name":"value","type":"uint256","internalType":"uint256"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setQueueConfig","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"priority","type":"uint32","internalType":"uint32"},{"name":"maxDeposits","type":"uint32","internalType":"uint32"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setRewardShareData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"data","type":"tuple[]","internalType":"struct ICSParametersRegistry.KeyNumberValueInterval[]","components":[{"name":"minKeyNumber","type":"uint256","internalType":"uint256"},{"name":"value","type":"uint256","internalType":"uint256"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setStrikesParams","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"lifetime","type":"uint256","internalType":"uint256"},{"name":"threshold","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"unsetAllowedExitDelay","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetBadPerformancePenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetElRewardsStealingAdditionalFine","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetExitDelayPenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetKeyRemovalCharge","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetKeysLimit","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetMaxWithdrawalRequestFee","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetPerformanceCoefficients","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetPerformanceLeewayData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetQueueConfig","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetRewardShareData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetStrikesParams","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"AllowedExitDelaySet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"delay","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"AllowedExitDelayUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BadPerformancePenaltySet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"penalty","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BadPerformancePenaltyUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultAllowedExitDelaySet","inputs":[{"name":"delay","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultBadPerformancePenaltySet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultElRewardsStealingAdditionalFineSet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultExitDelayPenaltySet","inputs":[{"name":"penalty","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultKeyRemovalChargeSet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultKeysLimitSet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultMaxWithdrawalRequestFeeSet","inputs":[{"name":"fee","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultPerformanceCoefficientsSet","inputs":[{"name":"attestationsWeight","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"blocksWeight","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"syncWeight","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultPerformanceLeewaySet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultQueueConfigSet","inputs":[{"name":"priority","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"maxDeposits","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultRewardShareSet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultStrikesParamsSet","inputs":[{"name":"lifetime","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"threshold","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ElRewardsStealingAdditionalFineSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"fine","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ElRewardsStealingAdditionalFineUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ExitDelayPenaltySet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"penalty","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ExitDelayPenaltyUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"KeyRemovalChargeSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"keyRemovalCharge","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"KeyRemovalChargeUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"KeysLimitSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"limit","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"KeysLimitUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"MaxWithdrawalRequestFeeSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"fee","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"MaxWithdrawalRequestFeeUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PerformanceCoefficientsSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"attestationsWeight","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"blocksWeight","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"syncWeight","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PerformanceCoefficientsUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PerformanceLeewayDataSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"data","type":"tuple[]","indexed":false,"internalType":"struct ICSParametersRegistry.KeyNumberValueInterval[]","components":[{"name":"minKeyNumber","type":"uint256","internalType":"uint256"},{"name":"value","type":"uint256","internalType":"uint256"}]}],"anonymous":false},{"type":"event","name":"PerformanceLeewayDataUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"QueueConfigSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"priority","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"maxDeposits","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"QueueConfigUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"RewardShareDataSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"data","type":"tuple[]","indexed":false,"internalType":"struct ICSParametersRegistry.KeyNumberValueInterval[]","components":[{"name":"minKeyNumber","type":"uint256","internalType":"uint256"},{"name":"value","type":"uint256","internalType":"uint256"}]}],"anonymous":false},{"type":"event","name":"RewardShareDataUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"StrikesParamsSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"lifetime","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"threshold","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"StrikesParamsUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"InvalidExitDelayPenalty","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidKeyIndexValueIntervals","inputs":[]},{"type":"error","name":"InvalidPerformanceCoefficients","inputs":[]},{"type":"error","name":"InvalidPerformanceLeewayData","inputs":[]},{"type":"error","name":"InvalidRewardShareData","inputs":[]},{"type":"error","name":"InvalidStrikesParams","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"QueueCannotBeUsed","inputs":[]},{"type":"error","name":"SafeCastOverflowedUintDowncast","inputs":[{"name":"bits","type":"uint8","internalType":"uint8"},{"name":"value","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroMaxDeposits","inputs":[]}] \ No newline at end of file diff --git a/assets/CSStrikes.json b/assets/CSStrikes.json index d9a852d9b..9f17ab33b 100644 --- a/assets/CSStrikes.json +++ b/assets/CSStrikes.json @@ -1 +1 @@ -[{"type":"constructor","inputs":[{"name":"module","type":"address","internalType":"address"},{"name":"oracle","type":"address","internalType":"address"}],"stateMutability":"nonpayable"},{"type":"function","name":"MODULE","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSModule"}],"stateMutability":"view"},{"type":"function","name":"ORACLE","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"hashLeaf","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"pubkey","type":"bytes","internalType":"bytes"},{"name":"strikesData","type":"uint256[]","internalType":"uint256[]"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"pure"},{"type":"function","name":"processBadPerformanceProof","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"},{"name":"strikesData","type":"uint256[]","internalType":"uint256[]"},{"name":"proof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"processOracleReport","inputs":[{"name":"_treeRoot","type":"bytes32","internalType":"bytes32"},{"name":"_treeCid","type":"string","internalType":"string"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"treeCid","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"treeRoot","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"verifyProof","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"pubkey","type":"bytes","internalType":"bytes"},{"name":"strikesData","type":"uint256[]","internalType":"uint256[]"},{"name":"proof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"event","name":"StrikesDataUpdated","inputs":[{"name":"treeRoot","type":"bytes32","indexed":false,"internalType":"bytes32"},{"name":"treeCid","type":"string","indexed":false,"internalType":"string"}],"anonymous":false},{"type":"event","name":"StrikesDataWiped","inputs":[],"anonymous":false},{"type":"error","name":"InvalidProof","inputs":[]},{"type":"error","name":"InvalidReportData","inputs":[]},{"type":"error","name":"NotOracle","inputs":[]},{"type":"error","name":"ZeroBadPerformancePenaltyAmount","inputs":[]},{"type":"error","name":"ZeroEjectionFeeAmount","inputs":[]},{"type":"error","name":"ZeroModuleAddress","inputs":[]},{"type":"error","name":"ZeroOracleAddress","inputs":[]}] +[{"type":"constructor","inputs":[{"name":"module","type":"address","internalType":"address"},{"name":"oracle","type":"address","internalType":"address"},{"name":"exitPenalties","type":"address","internalType":"address"}],"stateMutability":"nonpayable"},{"type":"function","name":"ACCOUNTING","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSAccounting"}],"stateMutability":"view"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"EXIT_PENALTIES","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSExitPenalties"}],"stateMutability":"view"},{"type":"function","name":"MODULE","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSModule"}],"stateMutability":"view"},{"type":"function","name":"ORACLE","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"PARAMETERS_REGISTRY","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSParametersRegistry"}],"stateMutability":"view"},{"type":"function","name":"ejector","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSEjector"}],"stateMutability":"view"},{"type":"function","name":"getInitializedVersion","inputs":[],"outputs":[{"name":"","type":"uint64","internalType":"uint64"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"hashLeaf","inputs":[{"name":"keyStrikes","type":"tuple","internalType":"struct ICSStrikes.ModuleKeyStrikes","components":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"},{"name":"data","type":"uint256[]","internalType":"uint256[]"}]},{"name":"pubkey","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"pure"},{"type":"function","name":"initialize","inputs":[{"name":"admin","type":"address","internalType":"address"},{"name":"_ejector","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"processBadPerformanceProof","inputs":[{"name":"keyStrikesList","type":"tuple[]","internalType":"struct ICSStrikes.ModuleKeyStrikes[]","components":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"},{"name":"data","type":"uint256[]","internalType":"uint256[]"}]},{"name":"proof","type":"bytes32[]","internalType":"bytes32[]"},{"name":"proofFlags","type":"bool[]","internalType":"bool[]"},{"name":"refundRecipient","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"processOracleReport","inputs":[{"name":"_treeRoot","type":"bytes32","internalType":"bytes32"},{"name":"_treeCid","type":"string","internalType":"string"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setEjector","inputs":[{"name":"_ejector","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"treeCid","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"treeRoot","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"verifyProof","inputs":[{"name":"keyStrikesList","type":"tuple[]","internalType":"struct ICSStrikes.ModuleKeyStrikes[]","components":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"},{"name":"data","type":"uint256[]","internalType":"uint256[]"}]},{"name":"pubkeys","type":"bytes[]","internalType":"bytes[]"},{"name":"proof","type":"bytes32[]","internalType":"bytes32[]"},{"name":"proofFlags","type":"bool[]","internalType":"bool[]"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"event","name":"EjectorSet","inputs":[{"name":"ejector","type":"address","indexed":false,"internalType":"address"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"StrikesDataUpdated","inputs":[{"name":"treeRoot","type":"bytes32","indexed":false,"internalType":"bytes32"},{"name":"treeCid","type":"string","indexed":false,"internalType":"string"}],"anonymous":false},{"type":"event","name":"StrikesDataWiped","inputs":[],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidProof","inputs":[]},{"type":"error","name":"InvalidReportData","inputs":[]},{"type":"error","name":"MerkleProofInvalidMultiproof","inputs":[]},{"type":"error","name":"NotEnoughStrikesToEject","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"NotOracle","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroBadPerformancePenaltyAmount","inputs":[]},{"type":"error","name":"ZeroEjectionFeeAmount","inputs":[]},{"type":"error","name":"ZeroEjectorAddress","inputs":[]},{"type":"error","name":"ZeroExitPenaltiesAddress","inputs":[]},{"type":"error","name":"ZeroModuleAddress","inputs":[]},{"type":"error","name":"ZeroOracleAddress","inputs":[]}] \ No newline at end of file diff --git a/src/providers/execution/contracts/cs_fee_oracle.py b/src/providers/execution/contracts/cs_fee_oracle.py index 824a76f6b..15d0c042f 100644 --- a/src/providers/execution/contracts/cs_fee_oracle.py +++ b/src/providers/execution/contracts/cs_fee_oracle.py @@ -28,10 +28,10 @@ def is_paused(self, block_identifier: BlockIdentifier = "latest") -> bool: def strikes(self, block_identifier: BlockIdentifier = "latest") -> ChecksumAddress: """Return the address of the CSStrikes contract""" - resp = self.functions.strikes().call(block_identifier=block_identifier) + resp = self.functions.STRIKES().call(block_identifier=block_identifier) logger.info( { - "msg": "Call `strikes()`.", + "msg": "Call `STRIKES()`.", "value": resp, "block_identifier": repr(block_identifier), } From 8fe4325b0ecc4cd3b885e5bd16c5dbc4c96e9f68 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Mon, 19 May 2025 11:18:10 +0200 Subject: [PATCH 130/162] refactor: sort active validators --- src/modules/csm/distribution.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/csm/distribution.py b/src/modules/csm/distribution.py index 077cd8bf4..ef3039281 100644 --- a/src/modules/csm/distribution.py +++ b/src/modules/csm/distribution.py @@ -140,8 +140,8 @@ def _calculate_distribution_in_frame( curve_params = self.w3.csm.get_curve_params(no_id, blockstamp) log_operator.performance_coefficients = curve_params.perf_coeffs - sorted_active_validators = sorted(active_validators, key=lambda v: v.index) - numbered_validators = enumerate(sorted_active_validators, 1) + active_validators.sort(key=lambda v: v.index) + numbered_validators = enumerate(active_validators, 1) for key_number, validator in numbered_validators: key_threshold = max(network_perf - curve_params.perf_leeway_data.get_for(key_number), 0) key_reward_share = curve_params.reward_share_data.get_for(key_number) From 84b0a6d7abdefdc70c0f13bf22b3ccbc52c92f6f Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Mon, 19 May 2025 11:49:18 +0200 Subject: [PATCH 131/162] chore: remove `FIXME` --- tests/fork/test_csm_oracle_cycle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fork/test_csm_oracle_cycle.py b/tests/fork/test_csm_oracle_cycle.py index 7c42c1cdb..634d84145 100644 --- a/tests/fork/test_csm_oracle_cycle.py +++ b/tests/fork/test_csm_oracle_cycle.py @@ -83,7 +83,7 @@ def update_csm_to_v2(accounts_from_fork, forked_el_client: Web3, anvil_port: int **os.environ, 'CHAIN': chain, "ANVIL_PORT": str(anvil_port), - "RPC_URL": f"http://127.0.0.1:{anvil_port}", # FIXME: actually unused by the script, remove when fixed + "RPC_URL": f"http://127.0.0.1:{anvil_port}", 'DEPLOY_CONFIG': f'./artifacts/local/upgrade-{chain}.json', }, stdout=subprocess.DEVNULL, From 2d607af39d3a70d434c96ab6739fb8d7601ce648 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Mon, 19 May 2025 11:49:37 +0200 Subject: [PATCH 132/162] fix: `KeyNumberValue` parsing --- .../execution/contracts/cs_parameters_registry.py | 14 +++++++------- .../contracts/test_cs_parameters_registry.py | 6 +++--- tests/modules/csm/test_csm_distribution.py | 12 ++++++------ 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/providers/execution/contracts/cs_parameters_registry.py b/src/providers/execution/contracts/cs_parameters_registry.py index 0b460e09c..59e79c346 100644 --- a/src/providers/execution/contracts/cs_parameters_registry.py +++ b/src/providers/execution/contracts/cs_parameters_registry.py @@ -48,7 +48,7 @@ class KeyNumberValueInterval: @dataclass -class IntervalMapping: +class KeyNumberValue: intervals: list[KeyNumberValueInterval] def get_for(self, key_number: int) -> float: @@ -69,8 +69,8 @@ class StrikesParams: @dataclass class CurveParams: perf_coeffs: PerformanceCoefficients - perf_leeway_data: IntervalMapping - reward_share_data: IntervalMapping + perf_leeway_data: KeyNumberValue + reward_share_data: KeyNumberValue strikes_params: StrikesParams @@ -100,7 +100,7 @@ def get_reward_share_data( self, curve_id: int, block_identifier: BlockIdentifier = "latest", - ) -> IntervalMapping: + ) -> KeyNumberValue: """Returns reward share data for given node operator""" resp = self.functions.getRewardShareData(curve_id).call(block_identifier=block_identifier) @@ -111,14 +111,14 @@ def get_reward_share_data( "block_identifier": repr(block_identifier), } ) - return IntervalMapping(intervals=[KeyNumberValueInterval(r.minKeyNumber, r.value) for r in resp]) + return KeyNumberValue(intervals=[KeyNumberValueInterval(r.minKeyNumber, r.value) for r in resp.intervals]) @lru_cache() def get_performance_leeway_data( self, curve_id: int, block_identifier: BlockIdentifier = "latest", - ) -> IntervalMapping: + ) -> KeyNumberValue: """Returns performance leeway data for given node operator""" resp = self.functions.getPerformanceLeewayData(curve_id).call(block_identifier=block_identifier) @@ -129,7 +129,7 @@ def get_performance_leeway_data( "block_identifier": repr(block_identifier), } ) - return IntervalMapping(intervals=[KeyNumberValueInterval(r.minKeyNumber, r.value) for r in resp]) + return KeyNumberValue(intervals=[KeyNumberValueInterval(r.minKeyNumber, r.value) for r in resp.intervals]) @lru_cache() def get_strikes_params( diff --git a/tests/integration/contracts/test_cs_parameters_registry.py b/tests/integration/contracts/test_cs_parameters_registry.py index 88a39e80f..2740019be 100644 --- a/tests/integration/contracts/test_cs_parameters_registry.py +++ b/tests/integration/contracts/test_cs_parameters_registry.py @@ -3,7 +3,7 @@ from src.providers.execution.contracts.cs_parameters_registry import ( PerformanceCoefficients, - IntervalMapping, + KeyNumberValue, StrikesParams, ) from tests.integration.contracts.contract_utils import check_contract, check_is_instance_of @@ -16,8 +16,8 @@ def test_cs_parameters_registry(cs_params_contract, caplog): cs_params_contract, [ ("get_performance_coefficients", None, check_is_instance_of(PerformanceCoefficients)), - ("get_reward_share_data", None, check_is_instance_of(IntervalMapping)), - ("get_performance_leeway_data", None, check_is_instance_of(IntervalMapping)), + ("get_reward_share_data", None, check_is_instance_of(KeyNumberValue)), + ("get_performance_leeway_data", None, check_is_instance_of(KeyNumberValue)), ("get_strikes_params", None, check_is_instance_of(StrikesParams)), ], caplog, diff --git a/tests/modules/csm/test_csm_distribution.py b/tests/modules/csm/test_csm_distribution.py index 26fcca1c0..39e2bfabb 100644 --- a/tests/modules/csm/test_csm_distribution.py +++ b/tests/modules/csm/test_csm_distribution.py @@ -16,7 +16,7 @@ PerformanceCoefficients, CurveParams, KeyNumberValueInterval, - IntervalMapping, + KeyNumberValue, ) from src.providers.execution.exceptions import InconsistentData from src.types import NodeOperatorId, EpochNumber, ValidatorIndex @@ -607,10 +607,10 @@ def test_calculate_distribution_handles_invalid_distribution_in_total(): side_effect=lambda no_id, _: { NodeOperatorId(5): CurveParams( strikes_params=..., - perf_leeway_data=IntervalMapping( + perf_leeway_data=KeyNumberValue( intervals=[KeyNumberValueInterval(1, 1000), KeyNumberValueInterval(2, 2000)] ), - reward_share_data=IntervalMapping( + reward_share_data=KeyNumberValue( intervals=[KeyNumberValueInterval(1, 10000), KeyNumberValueInterval(2, 9000)] ), perf_coeffs=PerformanceCoefficients(attestations_weight=1, blocks_weight=0, sync_weight=0), @@ -1160,13 +1160,13 @@ def test_performance_coefficients_calc_performance(attestation_perf, proposal_pe ) @pytest.mark.unit def test_interval_mapping_returns_correct_reward_share(intervals, key_index, expected): - reward_share = IntervalMapping(intervals=intervals) + reward_share = KeyNumberValue(intervals=intervals) assert reward_share.get_for(key_index) == expected @pytest.mark.unit def test_interval_mapping_raises_error_for_invalid_key_number(): - reward_share = IntervalMapping( + reward_share = KeyNumberValue( intervals=[KeyNumberValueInterval(1, 1000), KeyNumberValueInterval(11, 2000), KeyNumberValueInterval(21, 3000)] ) with pytest.raises(ValueError, match="Key number should be greater than 1 or equal"): @@ -1175,6 +1175,6 @@ def test_interval_mapping_raises_error_for_invalid_key_number(): @pytest.mark.unit def test_interval_mapping_raises_error_for_key_number_out_of_range(): - reward_share = IntervalMapping(intervals=[KeyNumberValueInterval(11, 10000)]) + reward_share = KeyNumberValue(intervals=[KeyNumberValueInterval(11, 10000)]) with pytest.raises(ValueError, match="No value found for key number=2"): reward_share.get_for(2) From f367d5b9b788e05e4e0b59ea1883d6df97ed64a5 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Tue, 20 May 2025 09:54:18 +0200 Subject: [PATCH 133/162] refactor: `PerformanceCoefficients` now uses constants --- src/constants.py | 5 +++++ .../execution/contracts/cs_parameters_registry.py | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/constants.py b/src/constants.py index eabc9f128..5e73ce9ba 100644 --- a/src/constants.py +++ b/src/constants.py @@ -47,6 +47,11 @@ SHARE_RATE_PRECISION_E27 = 10**27 TOTAL_BASIS_POINTS = 10000 +# Lido CSM constants for network performance calculation +ATTESTATIONS_WEIGHT = 54 +BLOCKS_WEIGHT = 8 +SYNC_WEIGHT = 2 + # Local constants GWEI_TO_WEI = 10**9 MAX_BLOCK_GAS_LIMIT = 30_000_000 diff --git a/src/providers/execution/contracts/cs_parameters_registry.py b/src/providers/execution/contracts/cs_parameters_registry.py index 59e79c346..1a1eb9a94 100644 --- a/src/providers/execution/contracts/cs_parameters_registry.py +++ b/src/providers/execution/contracts/cs_parameters_registry.py @@ -3,7 +3,7 @@ from web3.types import BlockIdentifier -from src.constants import TOTAL_BASIS_POINTS, UINT256_MAX +from src.constants import TOTAL_BASIS_POINTS, UINT256_MAX, ATTESTATIONS_WEIGHT, BLOCKS_WEIGHT, SYNC_WEIGHT from src.modules.csm.state import ValidatorDuties from src.providers.execution.base_interface import ContractInterface from src.utils.cache import global_lru_cache as lru_cache @@ -13,9 +13,9 @@ @dataclass class PerformanceCoefficients: - attestations_weight: int = 54 - blocks_weight: int = 8 - sync_weight: int = 2 + attestations_weight: int = ATTESTATIONS_WEIGHT + blocks_weight: int = BLOCKS_WEIGHT + sync_weight: int = SYNC_WEIGHT def calc_performance(self, duties: ValidatorDuties) -> float: base = 0 From d17fbe83928a43325a2097a077e98d8a41983682 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Tue, 20 May 2025 10:22:41 +0200 Subject: [PATCH 134/162] fix: use `persist-credentials` --- .github/workflows/mainnet_fork_tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/mainnet_fork_tests.yml b/.github/workflows/mainnet_fork_tests.yml index 203795e86..97bd5fafe 100644 --- a/.github/workflows/mainnet_fork_tests.yml +++ b/.github/workflows/mainnet_fork_tests.yml @@ -34,6 +34,7 @@ jobs: repository: 'lidofinance/community-staking-module' ref: 'develop' path: 'testruns/community-staking-module' + persist-credentials: false - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 From e2beee6d3334f76a1be712b6b317871ba1fa34fa Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Tue, 20 May 2025 10:23:47 +0200 Subject: [PATCH 135/162] refactor: type aliases, log, condition --- src/modules/csm/csm.py | 6 +++--- src/modules/csm/distribution.py | 24 +++++++++++----------- src/modules/csm/log.py | 8 ++++---- src/modules/csm/types.py | 5 +++-- tests/modules/csm/test_csm_distribution.py | 3 ++- 5 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index c11fae2b2..0e586084f 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -15,7 +15,7 @@ from src.modules.csm.log import FramePerfLog from src.modules.csm.state import State from src.modules.csm.tree import RewardsTree, StrikesTree, Tree -from src.modules.csm.types import ReportData, Shares, StrikesList +from src.modules.csm.types import ReportData, RewardsShares, StrikesList from src.modules.submodules.consensus import ConsensusModule from src.modules.submodules.oracle_module import BaseModule, ModuleExecuteDelay from src.modules.submodules.types import ZERO_HASH @@ -215,7 +215,7 @@ def collect_data(self, blockstamp: BlockStamp) -> bool: self._reset_cycle_timeout() return self.state.is_fulfilled - def make_rewards_tree(self, shares: dict[NodeOperatorId, Shares]) -> RewardsTree: + def make_rewards_tree(self, shares: dict[NodeOperatorId, RewardsShares]) -> RewardsTree: if not shares: raise ValueError("No shares to build a tree") @@ -233,7 +233,7 @@ def make_rewards_tree(self, shares: dict[NodeOperatorId, Shares]) -> RewardsTree logger.info({"msg": "New rewards tree built for the report", "root": repr(tree.root)}) return tree - def make_strikes_tree(self, strikes: dict[StrikesValidator, StrikesList]): + def make_strikes_tree(self, strikes: dict[StrikesValidator, StrikesList]) -> StrikesTree: if not strikes: raise ValueError("No strikes to build a tree") diff --git a/src/modules/csm/distribution.py b/src/modules/csm/distribution.py index ef3039281..8140bdba0 100644 --- a/src/modules/csm/distribution.py +++ b/src/modules/csm/distribution.py @@ -7,7 +7,7 @@ from src.modules.csm.helpers.last_report import LastReport from src.modules.csm.log import FramePerfLog, OperatorFrameSummary from src.modules.csm.state import Frame, State, ValidatorDuties -from src.modules.csm.types import Shares, StrikesList, StrikesValidator +from src.modules.csm.types import RewardsShares, StrikesList, StrikesValidator, ParticipationShares from src.providers.execution.contracts.cs_parameters_registry import PerformanceCoefficients from src.providers.execution.exceptions import InconsistentData from src.types import EpochNumber, NodeOperatorId, ReferenceBlockStamp, StakingModuleAddress @@ -21,16 +21,16 @@ @dataclass class ValidatorDutiesOutcome: - participation_share: int - rebate_share: int + participation_share: ParticipationShares + rebate_share: ParticipationShares strikes: int @dataclass class DistributionResult: - total_rewards: Shares = 0 - total_rebate: Shares = 0 - total_rewards_map: dict[NodeOperatorId, Shares] = field(default_factory=lambda: defaultdict(Shares)) + total_rewards: RewardsShares = 0 + total_rebate: RewardsShares = 0 + total_rewards_map: dict[NodeOperatorId, RewardsShares] = field(default_factory=lambda: defaultdict(RewardsShares)) strikes: dict[StrikesValidator, StrikesList] = field(default_factory=lambda: defaultdict(StrikesList)) logs: list[FramePerfLog] = field(default_factory=list) @@ -118,12 +118,12 @@ def _calculate_distribution_in_frame( self, frame: Frame, blockstamp: ReferenceBlockStamp, - rewards_to_distribute: Shares, + rewards_to_distribute: RewardsShares, operators_to_validators: ValidatorsByNodeOperator, log: FramePerfLog, - ) -> tuple[dict[NodeOperatorId, Shares], Shares, Shares, dict[StrikesValidator, int]]: + ) -> tuple[dict[NodeOperatorId, RewardsShares], RewardsShares, RewardsShares, dict[StrikesValidator, int]]: total_rebate_share = 0 - participation_shares: defaultdict[NodeOperatorId, Shares] = defaultdict(int) + participation_shares: defaultdict[NodeOperatorId, RewardsShares] = defaultdict(int) frame_strikes: dict[StrikesValidator, int] = {} network_perf = self._get_network_performance(frame) @@ -201,7 +201,7 @@ def get_validator_duties_outcome( log_validator = log_operator.validators[validator.index] - if validator.validator.slashed is True: + if validator.validator.slashed: # It means that validator was active during the frame and got slashed and didn't meet the exit # epoch, so we should not count such validator for operator's share. log_validator.slashed = True @@ -245,7 +245,7 @@ def calc_rewards_distribution_in_frame( participation_shares: dict[NodeOperatorId, int], rebate_share: int, rewards_to_distribute: int, - ) -> dict[NodeOperatorId, Shares]: + ) -> dict[NodeOperatorId, RewardsShares]: if rewards_to_distribute < 0: raise ValueError(f"Invalid rewards to distribute: {rewards_to_distribute=}") @@ -264,7 +264,7 @@ def calc_rewards_distribution_in_frame( def validate_distribution(total_distributed_rewards, total_rebate, total_rewards_to_distribute): if (total_distributed_rewards + total_rebate) > total_rewards_to_distribute: raise ValueError( - f"Invalid distribution: {total_distributed_rewards + total_rebate} > {total_rewards_to_distribute}" + f"Invalid distribution: {total_distributed_rewards} + {total_rebate} > {total_rewards_to_distribute}" ) def _process_strikes( diff --git a/src/modules/csm/log.py b/src/modules/csm/log.py index b2f7ebacb..997515ba7 100644 --- a/src/modules/csm/log.py +++ b/src/modules/csm/log.py @@ -3,7 +3,7 @@ from dataclasses import asdict, dataclass, field from src.modules.csm.state import DutyAccumulator -from src.modules.csm.types import Shares +from src.modules.csm.types import RewardsShares from src.providers.execution.contracts.cs_parameters_registry import PerformanceCoefficients from src.types import EpochNumber, NodeOperatorId, ReferenceBlockStamp, ValidatorIndex @@ -35,9 +35,9 @@ class FramePerfLog: """A log of performance assessed per operator in the given frame""" blockstamp: ReferenceBlockStamp frame: tuple[EpochNumber, EpochNumber] - distributable: Shares = 0 - distributed_rewards: Shares = 0 - rebate_to_protocol: Shares = 0 + distributable: RewardsShares = 0 + distributed_rewards: RewardsShares = 0 + rebate_to_protocol: RewardsShares = 0 operators: dict[NodeOperatorId, OperatorFrameSummary] = field( default_factory=lambda: defaultdict(OperatorFrameSummary) ) diff --git a/src/modules/csm/types.py b/src/modules/csm/types.py index 839f3fe0f..2ef3bad1d 100644 --- a/src/modules/csm/types.py +++ b/src/modules/csm/types.py @@ -47,8 +47,9 @@ def push(self, item: int) -> None: self.data.insert(0, item) -Shares: TypeAlias = int -type RewardsTreeLeaf = tuple[NodeOperatorId, Shares] +ParticipationShares: TypeAlias = int +RewardsShares: TypeAlias = int +type RewardsTreeLeaf = tuple[NodeOperatorId, RewardsShares] type StrikesTreeLeaf = tuple[NodeOperatorId, HexBytes, StrikesList] diff --git a/tests/modules/csm/test_csm_distribution.py b/tests/modules/csm/test_csm_distribution.py index 39e2bfabb..b33c93f96 100644 --- a/tests/modules/csm/test_csm_distribution.py +++ b/tests/modules/csm/test_csm_distribution.py @@ -1,3 +1,4 @@ +import re from collections import defaultdict from unittest.mock import Mock @@ -338,7 +339,7 @@ def test_calculate_distribution_handles_invalid_distribution(): ) ) - with pytest.raises(ValueError, match="Invalid distribution: 501 > 500"): + with pytest.raises(ValueError, match=re.escape("Invalid distribution: 500 + 1 > 500")): distribution.calculate(..., Mock(strikes={}, rewards=[])) From 1788e9432b73f65aa463528c54d47791f1a42e8e Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Tue, 20 May 2025 10:47:11 +0200 Subject: [PATCH 136/162] refactor: review --- src/modules/csm/checkpoint.py | 32 ++++++++++++++-------------- src/modules/csm/distribution.py | 18 ++++++++++------ src/modules/csm/state.py | 6 +++--- src/providers/consensus/client.py | 7 ++++-- tests/modules/csm/test_checkpoint.py | 30 +++++++++++++------------- tests/modules/csm/test_state.py | 24 ++++++++++----------- 6 files changed, 62 insertions(+), 55 deletions(-) diff --git a/src/modules/csm/checkpoint.py b/src/modules/csm/checkpoint.py index a506e9e47..021e71e18 100644 --- a/src/modules/csm/checkpoint.py +++ b/src/modules/csm/checkpoint.py @@ -115,9 +115,9 @@ def _is_min_step_reached(self): class SyncCommitteesCache(UserDict): - max_size = variables.CSM_ORACLE_MAX_CONCURRENCY + max_size = max(2, variables.CSM_ORACLE_MAX_CONCURRENCY) - def __setitem__(self, sync_committee_period: int, value: SyncCommittee | None): + def __setitem__(self, sync_committee_period: int, value: SyncCommittee): if len(self) >= self.max_size: self.pop(min(self)) super().__setitem__(sync_committee_period, value) @@ -205,7 +205,7 @@ def _process( checkpoint_block_roots: list[BlockRoot | None], checkpoint_slot: SlotNumber, unprocessed_epochs: list[EpochNumber], - duty_epochs_roots: dict[EpochNumber, tuple[list[SlotBlockRoot], list[SlotBlockRoot]]] + epochs_roots_to_check: dict[EpochNumber, tuple[list[SlotBlockRoot], list[SlotBlockRoot]]] ): executor = ThreadPoolExecutor(max_workers=variables.CSM_ORACLE_MAX_CONCURRENCY) try: @@ -215,7 +215,7 @@ def _process( checkpoint_block_roots, checkpoint_slot, duty_epoch, - *duty_epochs_roots[duty_epoch] + *epochs_roots_to_check[duty_epoch] ) for duty_epoch in unprocessed_epochs } @@ -240,39 +240,39 @@ def _check_duties( ): logger.info({"msg": f"Processing epoch {duty_epoch}"}) - att_committees = self._prepare_att_committees(duty_epoch) + att_committees = self._prepare_attestation_duties(duty_epoch) propose_duties = self._prepare_propose_duties(duty_epoch, checkpoint_block_roots, checkpoint_slot) - sync_committees = self._prepare_sync_committee(duty_epoch, duty_epoch_roots) + sync_committees = self._prepare_sync_committee_duties(duty_epoch, duty_epoch_roots) for slot, root in [*duty_epoch_roots, *next_epoch_roots]: missed_slot = root is None if missed_slot: continue attestations, sync_aggregate = self.cc.get_block_attestations_and_sync(root) - process_attestations(attestations, att_committees) if (slot, root) in duty_epoch_roots: propose_duties[slot].included = True process_sync(slot, sync_aggregate, sync_committees) + process_attestations(attestations, att_committees) with lock: if duty_epoch not in self.state.unprocessed_epochs: raise ValueError(f"Epoch {duty_epoch} is not in epochs that should be processed") for att_committee in att_committees.values(): for att_duty in att_committee: - self.state.increment_att_duty( + self.state.save_att_duty( duty_epoch, att_duty.validator_index, included=att_duty.included, ) for sync_committee in sync_committees.values(): for sync_duty in sync_committee: - self.state.increment_sync_duty( + self.state.save_sync_duty( duty_epoch, sync_duty.validator_index, included=sync_duty.included, ) for proposer_duty in propose_duties.values(): - self.state.increment_prop_duty( + self.state.save_prop_duty( duty_epoch, proposer_duty.validator_index, included=proposer_duty.included @@ -288,7 +288,7 @@ def _check_duties( {"msg": f"Attestation Committees for epoch {args.epoch} prepared in {duration:.2f} seconds"} ) ) - def _prepare_att_committees(self, epoch: EpochNumber) -> AttestationCommittees: + def _prepare_attestation_duties(self, epoch: EpochNumber) -> AttestationCommittees: committees = {} for committee in self.cc.get_attestation_committees(self.finalized_blockstamp, epoch): validators = [] @@ -303,21 +303,21 @@ def _prepare_att_committees(self, epoch: EpochNumber) -> AttestationCommittees: {"msg": f"Sync Committee for epoch {args.epoch} prepared in {duration:.2f} seconds"} ) ) - def _prepare_sync_committee( - self, epoch: EpochNumber, duty_block_roots: list[SlotBlockRoot] + def _prepare_sync_committee_duties( + self, epoch: EpochNumber, epoch_block_roots: list[SlotBlockRoot] ) -> dict[SlotNumber, list[ValidatorDuty]]: with lock: sync_committee = self._get_sync_committee(epoch) duties = {} - for slot, root in duty_block_roots: + for slot, root in epoch_block_roots: missed_slot = root is None if missed_slot: continue duties[slot] = [ - ValidatorDuty(validator_index=ValidatorIndex(int(validator)), included=False) - for validator in sync_committee.validators + ValidatorDuty(validator_index=validator_index, included=False) + for validator_index in sync_committee.validators ] return duties diff --git a/src/modules/csm/distribution.py b/src/modules/csm/distribution.py index 8140bdba0..42ce33459 100644 --- a/src/modules/csm/distribution.py +++ b/src/modules/csm/distribution.py @@ -72,7 +72,7 @@ def calculate(self, blockstamp: ReferenceBlockStamp, last_report: LastReport) -> result.strikes = self._process_strikes(result.strikes, strikes_in_frame, frame_blockstamp) if not strikes_in_frame: - logger.info({"msg": f"No strikes in frame [{from_epoch};{to_epoch}]"}) + logger.info({"msg": f"No strikes in frame [{from_epoch};{to_epoch}]. Just shifting current strikes."}) result.total_rewards += distributed_rewards_in_frame result.total_rebate += rebate_to_protocol_in_frame @@ -123,7 +123,7 @@ def _calculate_distribution_in_frame( log: FramePerfLog, ) -> tuple[dict[NodeOperatorId, RewardsShares], RewardsShares, RewardsShares, dict[StrikesValidator, int]]: total_rebate_share = 0 - participation_shares: defaultdict[NodeOperatorId, RewardsShares] = defaultdict(int) + participation_shares: defaultdict[NodeOperatorId, ParticipationShares] = defaultdict(ParticipationShares) frame_strikes: dict[StrikesValidator, int] = {} network_perf = self._get_network_performance(frame) @@ -197,6 +197,9 @@ def get_validator_duties_outcome( if duties.attestation is None or duties.attestation.assigned == 0: # It's possible that the validator is not assigned to any duty, hence it's performance # is not presented in the aggregates (e.g. exited, pending for activation etc). + # + # There is a case when validator is exited and still in sync committee. But we can't count his + # `participation_share` because there is no `assigned` attestations for him. return ValidatorDutiesOutcome(participation_share=0, rebate_share=0, strikes=0) log_validator = log_operator.validators[validator.index] @@ -233,7 +236,8 @@ def get_validator_duties_outcome( # participation_share = math.ceil(duties.attestation.assigned * reward_share) rebate_share = duties.attestation.assigned - participation_share - assert rebate_share >= 0, f"Invalid rebate share: {rebate_share=}" + if rebate_share < 0: + raise ValueError(f"Invalid rebate share: {rebate_share=}") return ValidatorDutiesOutcome(participation_share, rebate_share, strikes=0) # In case of bad performance the validator should be striked and assigned attestations are not counted for @@ -242,14 +246,14 @@ def get_validator_duties_outcome( @staticmethod def calc_rewards_distribution_in_frame( - participation_shares: dict[NodeOperatorId, int], - rebate_share: int, - rewards_to_distribute: int, + participation_shares: dict[NodeOperatorId, ParticipationShares], + rebate_share: ParticipationShares, + rewards_to_distribute: RewardsShares, ) -> dict[NodeOperatorId, RewardsShares]: if rewards_to_distribute < 0: raise ValueError(f"Invalid rewards to distribute: {rewards_to_distribute=}") - rewards_distribution: dict[NodeOperatorId, int] = defaultdict(int) + rewards_distribution: dict[NodeOperatorId, RewardsShares] = defaultdict(RewardsShares) total_shares = rebate_share + sum(participation_shares.values()) for no_id, no_participation_share in participation_shares.items(): diff --git a/src/modules/csm/state.py b/src/modules/csm/state.py index 35fd28735..595e6dbff 100644 --- a/src/modules/csm/state.py +++ b/src/modules/csm/state.py @@ -163,15 +163,15 @@ def find_frame(self, epoch: EpochNumber) -> Frame: return epoch_range raise ValueError(f"Epoch {epoch} is out of frames range: {self.frames}") - def increment_att_duty(self, epoch: EpochNumber, val_index: ValidatorIndex, included: bool) -> None: + def save_att_duty(self, epoch: EpochNumber, val_index: ValidatorIndex, included: bool) -> None: frame = self.find_frame(epoch) self.data[frame].attestations[val_index].add_duty(included) - def increment_prop_duty(self, epoch: EpochNumber, val_index: ValidatorIndex, included: bool) -> None: + def save_prop_duty(self, epoch: EpochNumber, val_index: ValidatorIndex, included: bool) -> None: frame = self.find_frame(epoch) self.data[frame].proposals[val_index].add_duty(included) - def increment_sync_duty(self, epoch: EpochNumber, val_index: ValidatorIndex, included: bool) -> None: + def save_sync_duty(self, epoch: EpochNumber, val_index: ValidatorIndex, included: bool) -> None: frame = self.find_frame(epoch) self.data[frame].syncs[val_index].add_duty(included) diff --git a/src/providers/consensus/client.py b/src/providers/consensus/client.py index 5b379fdf4..65cbf08e5 100644 --- a/src/providers/consensus/client.py +++ b/src/providers/consensus/client.py @@ -126,10 +126,13 @@ def get_block_attestations_and_sync(self, state_id: SlotNumber | BlockRoot) -> t if not isinstance(data, dict): raise ValueError("Expected mapping response from getBlockV2") - attestations = [BlockAttestationResponse.from_response(**att) for att in data["message"]["body"]["attestations"]] + attestations = [ + cast(BlockAttestation, BlockAttestationResponse.from_response(**att)) + for att in data["message"]["body"]["attestations"] + ] sync = SyncAggregate.from_response(**data["message"]["body"]["sync_aggregate"]) - return cast(list[BlockAttestation], attestations), sync + return attestations, sync @list_of_dataclasses(SlotAttestationCommittee.from_response) def get_attestation_committees( diff --git a/tests/modules/csm/test_checkpoint.py b/tests/modules/csm/test_checkpoint.py index 45776d419..7a9c53f1c 100644 --- a/tests/modules/csm/test_checkpoint.py +++ b/tests/modules/csm/test_checkpoint.py @@ -258,7 +258,7 @@ def test_checkpoints_processor_prepare_committees(mock_get_attestation_committee finalized_blockstamp, ) raw = consensus_client.get_attestation_committees(0, 0) - committees = processor._prepare_att_committees(0) + committees = processor._prepare_attestation_duties(0) assert len(committees) == 2048 for index, (committee_id, validators) in enumerate(committees.items()): slot, committee_index = committee_id @@ -280,7 +280,7 @@ def test_checkpoints_processor_process_attestations(mock_get_attestation_committ converter, finalized_blockstamp, ) - committees = processor._prepare_att_committees(0) + committees = processor._prepare_attestation_duties(0) # normal attestation attestation = cast(BlockAttestation, BlockAttestationFactory.build()) attestation.data.slot = 0 @@ -314,7 +314,7 @@ def test_checkpoints_processor_process_attestations_undefined_committee( converter, finalized_blockstamp, ) - committees = processor._prepare_att_committees(0) + committees = processor._prepare_attestation_duties(0) # undefined committee attestation = cast(BlockAttestation, BlockAttestationFactory.build()) attestation.data.slot = 100500 @@ -342,11 +342,11 @@ def test_check_duties_processes_epoch_with_attestations_and_sync_committee(frame duty_epoch = EpochNumber(10) duty_epoch_roots = [(SlotNumber(100), Mock(spec=BlockRoot)), (SlotNumber(101), Mock(spec=BlockRoot))] next_epoch_roots = [(SlotNumber(102), Mock(spec=BlockRoot)), (SlotNumber(103), Mock(spec=BlockRoot))] - frame_checkpoint_processor._prepare_att_committees = Mock(return_value={SlotNumber(100): [ValidatorDuty(1, False)]}) + frame_checkpoint_processor._prepare_attestation_duties = Mock(return_value={SlotNumber(100): [ValidatorDuty(1, False)]}) frame_checkpoint_processor._prepare_propose_duties = Mock( return_value={SlotNumber(100): ValidatorDuty(1, False), SlotNumber(101): ValidatorDuty(1, False)} ) - frame_checkpoint_processor._prepare_sync_committee = Mock( + frame_checkpoint_processor._prepare_sync_committee_duties = Mock( return_value={ 100: [ValidatorDuty(1, False) for _ in range(32)], 101: [ValidatorDuty(1, False) for _ in range(32)], @@ -369,9 +369,9 @@ def test_check_duties_processes_epoch_with_attestations_and_sync_committee(frame checkpoint_block_roots, checkpoint_slot, duty_epoch, duty_epoch_roots, next_epoch_roots ) - frame_checkpoint_processor.state.increment_att_duty.assert_called() - frame_checkpoint_processor.state.increment_sync_duty.assert_called() - frame_checkpoint_processor.state.increment_prop_duty.assert_called() + frame_checkpoint_processor.state.save_att_duty.assert_called() + frame_checkpoint_processor.state.save_sync_duty.assert_called() + frame_checkpoint_processor.state.save_prop_duty.assert_called() @pytest.mark.unit @@ -381,11 +381,11 @@ def test_check_duties_processes_epoch_with_no_attestations(frame_checkpoint_proc duty_epoch = EpochNumber(10) duty_epoch_roots = [(SlotNumber(100), Mock(spec=BlockRoot)), (SlotNumber(101), Mock(spec=BlockRoot))] next_epoch_roots = [(SlotNumber(102), Mock(spec=BlockRoot)), (SlotNumber(103), Mock(spec=BlockRoot))] - frame_checkpoint_processor._prepare_att_committees = Mock(return_value={}) + frame_checkpoint_processor._prepare_attestation_duties = Mock(return_value={}) frame_checkpoint_processor._prepare_propose_duties = Mock( return_value={SlotNumber(100): ValidatorDuty(1, False), SlotNumber(101): ValidatorDuty(1, False)} ) - frame_checkpoint_processor._prepare_sync_committee = Mock( + frame_checkpoint_processor._prepare_sync_committee_duties = Mock( return_value={100: [ValidatorDuty(1, False)], 101: [ValidatorDuty(1, False)]} ) @@ -399,9 +399,9 @@ def test_check_duties_processes_epoch_with_no_attestations(frame_checkpoint_proc checkpoint_block_roots, checkpoint_slot, duty_epoch, duty_epoch_roots, next_epoch_roots ) - assert frame_checkpoint_processor.state.increment_att_duty.call_count == 0 - assert frame_checkpoint_processor.state.increment_sync_duty.call_count == 2 - assert frame_checkpoint_processor.state.increment_prop_duty.call_count == 2 + assert frame_checkpoint_processor.state.save_att_duty.call_count == 0 + assert frame_checkpoint_processor.state.save_sync_duty.call_count == 2 + assert frame_checkpoint_processor.state.save_prop_duty.call_count == 2 @pytest.mark.unit @@ -412,7 +412,7 @@ def test_prepare_sync_committee_returns_duties_for_valid_sync_committee(frame_ch sync_committee.validators = [1, 2, 3] frame_checkpoint_processor._get_sync_committee = Mock(return_value=sync_committee) - duties = frame_checkpoint_processor._prepare_sync_committee(epoch, duty_block_roots) + duties = frame_checkpoint_processor._prepare_sync_committee_duties(epoch, duty_block_roots) expected_duties = { SlotNumber(100): [ @@ -437,7 +437,7 @@ def test_prepare_sync_committee_skips_duties_for_missed_slots(frame_checkpoint_p sync_committee.validators = [1, 2, 3] frame_checkpoint_processor._get_sync_committee = Mock(return_value=sync_committee) - duties = frame_checkpoint_processor._prepare_sync_committee(epoch, duty_block_roots) + duties = frame_checkpoint_processor._prepare_sync_committee_duties(epoch, duty_block_roots) expected_duties = { SlotNumber(101): [ diff --git a/tests/modules/csm/test_state.py b/tests/modules/csm/test_state.py index c9cdaf08c..26127842b 100644 --- a/tests/modules/csm/test_state.py +++ b/tests/modules/csm/test_state.py @@ -175,7 +175,7 @@ def test_increment_att_duty_adds_duty_correctly(): state.data = { frame: NetworkDuties(attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})), } - state.increment_att_duty(duty_epoch, ValidatorIndex(1), True) + state.save_att_duty(duty_epoch, ValidatorIndex(1), True) assert state.data[frame].attestations[ValidatorIndex(1)].assigned == 11 assert state.data[frame].attestations[ValidatorIndex(1)].included == 6 @@ -189,7 +189,7 @@ def test_increment_prop_duty_adds_duty_correctly(): state.data = { frame: NetworkDuties(proposals=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})), } - state.increment_prop_duty(duty_epoch, ValidatorIndex(1), True) + state.save_prop_duty(duty_epoch, ValidatorIndex(1), True) assert state.data[frame].proposals[ValidatorIndex(1)].assigned == 11 assert state.data[frame].proposals[ValidatorIndex(1)].included == 6 @@ -203,7 +203,7 @@ def test_increment_sync_duty_adds_duty_correctly(): state.data = { frame: NetworkDuties(syncs=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})), } - state.increment_sync_duty(duty_epoch, ValidatorIndex(1), True) + state.save_sync_duty(duty_epoch, ValidatorIndex(1), True) assert state.data[frame].syncs[ValidatorIndex(1)].assigned == 11 assert state.data[frame].syncs[ValidatorIndex(1)].included == 6 @@ -217,7 +217,7 @@ def test_increment_att_duty_creates_new_validator_entry(): state.data = { frame: NetworkDuties(), } - state.increment_att_duty(duty_epoch, ValidatorIndex(2), True) + state.save_att_duty(duty_epoch, ValidatorIndex(2), True) assert state.data[frame].attestations[ValidatorIndex(2)].assigned == 1 assert state.data[frame].attestations[ValidatorIndex(2)].included == 1 @@ -231,7 +231,7 @@ def test_increment_prop_duty_creates_new_validator_entry(): state.data = { frame: NetworkDuties(), } - state.increment_prop_duty(duty_epoch, ValidatorIndex(2), True) + state.save_prop_duty(duty_epoch, ValidatorIndex(2), True) assert state.data[frame].proposals[ValidatorIndex(2)].assigned == 1 assert state.data[frame].proposals[ValidatorIndex(2)].included == 1 @@ -245,7 +245,7 @@ def test_increment_sync_duty_creates_new_validator_entry(): state.data = { frame: NetworkDuties(), } - state.increment_sync_duty(duty_epoch, ValidatorIndex(2), True) + state.save_sync_duty(duty_epoch, ValidatorIndex(2), True) assert state.data[frame].syncs[ValidatorIndex(2)].assigned == 1 assert state.data[frame].syncs[ValidatorIndex(2)].included == 1 @@ -259,7 +259,7 @@ def test_increment_att_duty_handles_non_included_duty(): state.data = { frame: NetworkDuties(attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})), } - state.increment_att_duty(duty_epoch, ValidatorIndex(1), False) + state.save_att_duty(duty_epoch, ValidatorIndex(1), False) assert state.data[frame].attestations[ValidatorIndex(1)].assigned == 11 assert state.data[frame].attestations[ValidatorIndex(1)].included == 5 @@ -273,7 +273,7 @@ def test_increment_prop_duty_handles_non_included_duty(): state.data = { frame: NetworkDuties(proposals=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})), } - state.increment_prop_duty(duty_epoch, ValidatorIndex(1), False) + state.save_prop_duty(duty_epoch, ValidatorIndex(1), False) assert state.data[frame].proposals[ValidatorIndex(1)].assigned == 11 assert state.data[frame].proposals[ValidatorIndex(1)].included == 5 @@ -287,7 +287,7 @@ def test_increment_sync_duty_handles_non_included_duty(): state.data = { frame: NetworkDuties(syncs=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})), } - state.increment_sync_duty(duty_epoch, ValidatorIndex(1), False) + state.save_sync_duty(duty_epoch, ValidatorIndex(1), False) assert state.data[frame].syncs[ValidatorIndex(1)].assigned == 11 assert state.data[frame].syncs[ValidatorIndex(1)].included == 5 @@ -301,7 +301,7 @@ def test_increment_att_duty_raises_error_for_out_of_range_epoch(): (0, 31): defaultdict(DutyAccumulator), } with pytest.raises(ValueError, match="is out of frames range"): - state.increment_att_duty(32, ValidatorIndex(1), True) + state.save_att_duty(32, ValidatorIndex(1), True) @pytest.mark.unit @@ -313,7 +313,7 @@ def test_increment_prop_duty_raises_error_for_out_of_range_epoch(): (0, 31): defaultdict(DutyAccumulator), } with pytest.raises(ValueError, match="is out of frames range"): - state.increment_prop_duty(32, ValidatorIndex(1), True) + state.save_prop_duty(32, ValidatorIndex(1), True) @pytest.mark.unit @@ -325,7 +325,7 @@ def test_increment_sync_duty_raises_error_for_out_of_range_epoch(): (0, 31): defaultdict(DutyAccumulator), } with pytest.raises(ValueError, match="is out of frames range"): - state.increment_sync_duty(32, ValidatorIndex(1), True) + state.save_sync_duty(32, ValidatorIndex(1), True) @pytest.mark.unit From 2006c6cf97c4a656d46b9ede54ce54531979638d Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Tue, 20 May 2025 10:51:13 +0200 Subject: [PATCH 137/162] fix: linter --- tests/modules/csm/test_checkpoint.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/modules/csm/test_checkpoint.py b/tests/modules/csm/test_checkpoint.py index 7a9c53f1c..de3496545 100644 --- a/tests/modules/csm/test_checkpoint.py +++ b/tests/modules/csm/test_checkpoint.py @@ -342,7 +342,9 @@ def test_check_duties_processes_epoch_with_attestations_and_sync_committee(frame duty_epoch = EpochNumber(10) duty_epoch_roots = [(SlotNumber(100), Mock(spec=BlockRoot)), (SlotNumber(101), Mock(spec=BlockRoot))] next_epoch_roots = [(SlotNumber(102), Mock(spec=BlockRoot)), (SlotNumber(103), Mock(spec=BlockRoot))] - frame_checkpoint_processor._prepare_attestation_duties = Mock(return_value={SlotNumber(100): [ValidatorDuty(1, False)]}) + frame_checkpoint_processor._prepare_attestation_duties = Mock( + return_value={SlotNumber(100): [ValidatorDuty(1, False)]} + ) frame_checkpoint_processor._prepare_propose_duties = Mock( return_value={SlotNumber(100): ValidatorDuty(1, False), SlotNumber(101): ValidatorDuty(1, False)} ) From b0383413bdbdf2e7a6290ee91b63bab9c512066f Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Tue, 20 May 2025 17:07:28 +0200 Subject: [PATCH 138/162] refactor: a bunch of review changes --- src/modules/csm/csm.py | 21 +++++++++++++-------- src/modules/csm/state.py | 19 ++++++++++--------- tests/modules/csm/test_csm_module.py | 13 +++++++++++++ 3 files changed, 36 insertions(+), 17 deletions(-) diff --git a/src/modules/csm/csm.py b/src/modules/csm/csm.py index 0e586084f..f1183dcce 100644 --- a/src/modules/csm/csm.py +++ b/src/modules/csm/csm.py @@ -10,7 +10,7 @@ ) from src.metrics.prometheus.duration_meter import duration_meter from src.modules.csm.checkpoint import FrameCheckpointProcessor, FrameCheckpointsIterator, MinStepIsNotReached -from src.modules.csm.distribution import Distribution, StrikesValidator, DistributionResult +from src.modules.csm.distribution import Distribution, DistributionResult, StrikesValidator from src.modules.csm.helpers.last_report import LastReport from src.modules.csm.log import FramePerfLog from src.modules.csm.state import State @@ -70,6 +70,9 @@ def refresh_contracts(self): self.state.clear() def execute_module(self, last_finalized_blockstamp: BlockStamp) -> ModuleExecuteDelay: + if not self._check_compatability(last_finalized_blockstamp): + return ModuleExecuteDelay.NEXT_FINALIZED_EPOCH + collected = self.collect_data(last_finalized_blockstamp) if not collected: logger.info( @@ -77,9 +80,8 @@ def execute_module(self, last_finalized_blockstamp: BlockStamp) -> ModuleExecute ) return ModuleExecuteDelay.NEXT_FINALIZED_EPOCH - # pylint:disable=duplicate-code report_blockstamp = self.get_blockstamp_for_report(last_finalized_blockstamp) - if not report_blockstamp or not self._check_compatability(report_blockstamp): + if not report_blockstamp: return ModuleExecuteDelay.NEXT_FINALIZED_EPOCH self.process_report(report_blockstamp) @@ -105,9 +107,9 @@ def build_report(self, blockstamp: ReferenceBlockStamp) -> tuple: strikes_tree_root = strikes_tree.root strikes_cid = self.publish_tree(strikes_tree) if strikes_tree_root == last_report.strikes_tree_root: - logger.info({"msg": "Strikes tree root is the same as the previous one"}) - if strikes_cid == last_report.strikes_tree_cid: - logger.info({"msg": "Strikes tree CID is the same as the previous one"}) + logger.info({"msg": "Strikes tree is the same as the previous one"}) + if (strikes_cid == last_report.strikes_tree_cid) != (strikes_tree_root == last_report.strikes_tree_root): + raise ValueError(f"Invalid strikes tree built: {strikes_cid=}, {strikes_tree_root=}") else: strikes_tree_root = HexBytes(ZERO_HASH) strikes_cid = None @@ -199,7 +201,10 @@ def collect_data(self, blockstamp: BlockStamp) -> bool: try: checkpoints = FrameCheckpointsIterator( - converter, min(self.state.unprocessed_epochs) or l_epoch, r_epoch, finalized_epoch + converter, + min(self.state.unprocessed_epochs), + r_epoch, + finalized_epoch, ) except MinStepIsNotReached: return False @@ -229,7 +234,7 @@ def make_rewards_tree(self, shares: dict[NodeOperatorId, RewardsShares]) -> Rewa if stone in shares and len(shares) > 2: shares.pop(stone) - tree = RewardsTree.new(tuple((no_id, amount) for (no_id, amount) in shares.items())) + tree = RewardsTree.new(tuple(shares.items())) logger.info({"msg": "New rewards tree built for the report", "root": repr(tree.root)}) return tree diff --git a/src/modules/csm/state.py b/src/modules/csm/state.py index 595e6dbff..cef07f3d2 100644 --- a/src/modules/csm/state.py +++ b/src/modules/csm/state.py @@ -48,6 +48,7 @@ class ValidatorDuties: @dataclass class NetworkDuties: + # fmt: off attestations: defaultdict[ValidatorIndex, DutyAccumulator] = field(default_factory=lambda: defaultdict(DutyAccumulator)) proposals: defaultdict[ValidatorIndex, DutyAccumulator] = field(default_factory=lambda: defaultdict(DutyAccumulator)) syncs: defaultdict[ValidatorIndex, DutyAccumulator] = field(default_factory=lambda: defaultdict(DutyAccumulator)) @@ -75,19 +76,21 @@ class State: The state can be migrated to be used for another frame's report by calling the `migrate` method. """ + frames: list[Frame] data: StateData _epochs_to_process: tuple[EpochNumber, ...] _processed_epochs: set[EpochNumber] - _consensus_version: int = 2 + _consensus_version: int def __init__(self) -> None: self.frames = [] self.data = {} self._epochs_to_process = tuple() self._processed_epochs = set() + self._consensus_version = 0 EXTENSION = ".pkl" @@ -100,12 +103,13 @@ def load(cls) -> Self: try: with file.open(mode="rb") as f: obj = pickle.load(f) + print({"msg": "Read object from pickle file"}) if not obj: raise ValueError("Got empty object") except Exception as e: # pylint: disable=broad-exception-caught - logger.info({"msg": f"Unable to restore {cls.__name__} instance from {file.absolute()}", "error": str(e)}) + print({"msg": f"Unable to restore {cls.__name__} instance from {file.absolute()}", "error": str(e)}) else: - logger.info({"msg": f"{cls.__name__} read from {file.absolute()}"}) + print({"msg": f"{cls.__name__} read from {file.absolute()}"}) return obj or cls() def commit(self) -> None: @@ -124,11 +128,7 @@ def buffer(self) -> Path: @property def is_empty(self) -> bool: - return ( - not self.data and - not self._epochs_to_process and - not self._processed_epochs - ) + return not self.data and not self._epochs_to_process and not self._processed_epochs @property def unprocessed_epochs(self) -> set[EpochNumber]: @@ -153,6 +153,7 @@ def clear(self) -> None: self.data = {} self._epochs_to_process = tuple() self._processed_epochs.clear() + self._consensus_version = 0 assert self.is_empty @lru_cache(variables.CSM_ORACLE_MAX_CONCURRENCY) @@ -184,7 +185,7 @@ def log_progress(self) -> None: def migrate( self, l_epoch: EpochNumber, r_epoch: EpochNumber, epochs_per_frame: int, consensus_version: int ) -> None: - if consensus_version != self._consensus_version: + if self._consensus_version and consensus_version != self._consensus_version: logger.warning( { "msg": f"Cache was built for consensus version {self._consensus_version}. " diff --git a/tests/modules/csm/test_csm_module.py b/tests/modules/csm/test_csm_module.py index b1ccb2fcf..8b1bdfa93 100644 --- a/tests/modules/csm/test_csm_module.py +++ b/tests/modules/csm/test_csm_module.py @@ -586,16 +586,29 @@ def test_build_report(module: CSOracle, param: BuildReportTestParam): @pytest.mark.unit def test_execute_module_not_collected(module: CSOracle): + module._check_compatability = Mock(return_value=True) + module.collect_data = Mock(return_value=False) + + execute_delay = module.execute_module( + last_finalized_blockstamp=Mock(slot_number=100500), + ) + assert execute_delay is ModuleExecuteDelay.NEXT_FINALIZED_EPOCH + +@pytest.mark.unit +def test_execute_module_skips_collecting_if_forward_compatible(module: CSOracle): + module._check_compatability = Mock(return_value=False) module.collect_data = Mock(return_value=False) execute_delay = module.execute_module( last_finalized_blockstamp=Mock(slot_number=100500), ) assert execute_delay is ModuleExecuteDelay.NEXT_FINALIZED_EPOCH + module.collect_data.assert_not_called() @pytest.mark.unit def test_execute_module_no_report_blockstamp(module: CSOracle): + module._check_compatability = Mock(return_value=True) module.collect_data = Mock(return_value=True) module.get_blockstamp_for_report = Mock(return_value=None) From 7b5461b1e1bd88acd8fec93d5ca2589c4e588966 Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Tue, 20 May 2025 18:51:58 +0200 Subject: [PATCH 139/162] fix: return logging back --- src/modules/csm/state.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/csm/state.py b/src/modules/csm/state.py index cef07f3d2..974437589 100644 --- a/src/modules/csm/state.py +++ b/src/modules/csm/state.py @@ -107,9 +107,9 @@ def load(cls) -> Self: if not obj: raise ValueError("Got empty object") except Exception as e: # pylint: disable=broad-exception-caught - print({"msg": f"Unable to restore {cls.__name__} instance from {file.absolute()}", "error": str(e)}) + logger.info({"msg": f"Unable to restore {cls.__name__} instance from {file.absolute()}", "error": str(e)}) else: - print({"msg": f"{cls.__name__} read from {file.absolute()}"}) + logger.info({"msg": f"{cls.__name__} read from {file.absolute()}"}) return obj or cls() def commit(self) -> None: From 60c1d737dc35b4b4b03840e00a90669dd038db4f Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Wed, 21 May 2025 19:37:49 +0200 Subject: [PATCH 140/162] feat: check for list/dict response in http provider --- src/providers/consensus/client.py | 80 ++++----- src/providers/http_provider.py | 158 +++++++++++++++++- src/providers/keys/client.py | 25 +-- .../consensus/test_consensus_client.py | 10 +- tests/providers_clients/test_http_provider.py | 18 +- 5 files changed, 221 insertions(+), 70 deletions(-) diff --git a/src/providers/consensus/client.py b/src/providers/consensus/client.py index 65cbf08e5..46935da2c 100644 --- a/src/providers/consensus/client.py +++ b/src/providers/consensus/client.py @@ -1,12 +1,11 @@ from http import HTTPStatus from typing import Literal, cast -from json_stream.base import TransientStreamingJSONObject # type: ignore - from src import variables from src.metrics.logging import logging from src.metrics.prometheus.basic import CL_REQUESTS_DURATION from src.providers.consensus.types import ( + BeaconSpecResponse, BeaconStateView, BlockAttestation, BlockAttestationResponse, @@ -14,16 +13,15 @@ BlockHeaderFullResponse, BlockHeaderResponseData, BlockRootResponse, - Validator, - BeaconSpecResponse, GenesisResponse, - SlotAttestationCommittee, ProposerDuties, - SyncCommittee, + SlotAttestationCommittee, SyncAggregate, + SyncCommittee, + Validator, ) from src.providers.http_provider import HTTPProvider, NotOkResponse -from src.types import BlockRoot, BlockStamp, SlotNumber, EpochNumber, StateRoot +from src.types import BlockRoot, BlockStamp, EpochNumber, SlotNumber, StateRoot from src.utils.cache import global_lru_cache as lru_cache from src.utils.dataclass import list_of_dataclasses @@ -61,18 +59,14 @@ class ConsensusClient(HTTPProvider): def get_config_spec(self) -> BeaconSpecResponse: """Spec: https://ethereum.github.io/beacon-APIs/#/Config/getSpec""" - data, _ = self._get(self.API_GET_SPEC) - if not isinstance(data, dict): - raise ValueError("Expected mapping response from getSpec") + data, _ = self._get(self.API_GET_SPEC, is_dict=True) return BeaconSpecResponse.from_response(**data) def get_genesis(self): """ Spec: https://ethereum.github.io/beacon-APIs/#/Beacon/getGenesis """ - data, _ = self._get(self.API_GET_GENESIS) - if not isinstance(data, dict): - raise ValueError("Expected mapping response from getGenesis") + data, _ = self._get(self.API_GET_GENESIS, is_dict=True) return GenesisResponse.from_response(**data) def get_block_root(self, state_id: SlotNumber | BlockRoot | LiteralState) -> BlockRootResponse: @@ -85,21 +79,19 @@ def get_block_root(self, state_id: SlotNumber | BlockRoot | LiteralState) -> Blo self.API_GET_BLOCK_ROOT, path_params=(state_id,), force_raise=self.__raise_last_missed_slot_error, + is_dict=True, ) - if not isinstance(data, dict): - raise ValueError("Expected mapping response from getBlockRoot") return BlockRootResponse.from_response(**data) @lru_cache(maxsize=1) def get_block_header(self, state_id: SlotNumber | BlockRoot) -> BlockHeaderFullResponse: """Spec: https://ethereum.github.io/beacon-APIs/#/Beacon/getBlockHeader""" - data, meta_data = cast(tuple[dict, dict], self._get( + data, meta_data = self._get( self.API_GET_BLOCK_HEADER, path_params=(state_id,), force_raise=self.__raise_last_missed_slot_error, - )) - if not isinstance(data, dict): - raise ValueError("Expected mapping response from getBlockHeader") + is_dict=True, + ) resp = BlockHeaderFullResponse.from_response(data=BlockHeaderResponseData.from_response(**data), **meta_data) return resp @@ -110,21 +102,21 @@ def get_block_details(self, state_id: SlotNumber | BlockRoot) -> BlockDetailsRes self.API_GET_BLOCK_DETAILS, path_params=(state_id,), force_raise=self.__raise_last_missed_slot_error, + is_dict=True, ) - if not isinstance(data, dict): - raise ValueError("Expected mapping response from getBlockV2") return BlockDetailsResponse.from_response(**data) @lru_cache(maxsize=variables.CSM_ORACLE_MAX_CONCURRENCY * 32 * 2) # threads count * blocks * epochs to check duties - def get_block_attestations_and_sync(self, state_id: SlotNumber | BlockRoot) -> tuple[list[BlockAttestation], SyncAggregate]: + def get_block_attestations_and_sync( + self, state_id: SlotNumber | BlockRoot + ) -> tuple[list[BlockAttestation], SyncAggregate]: """Spec: https://ethereum.github.io/beacon-APIs/#/Beacon/getBlockV2""" data, _ = self._get( self.API_GET_BLOCK_DETAILS, path_params=(state_id,), force_raise=self.__raise_last_missed_slot_error, + is_dict=True, ) - if not isinstance(data, dict): - raise ValueError("Expected mapping response from getBlockV2") attestations = [ cast(BlockAttestation, BlockAttestationResponse.from_response(**att)) @@ -140,7 +132,7 @@ def get_attestation_committees( blockstamp: BlockStamp, epoch: EpochNumber | None = None, committee_index: int | None = None, - slot: SlotNumber | None = None + slot: SlotNumber | None = None, ) -> list[SlotAttestationCommittee]: """Spec: https://ethereum.github.io/beacon-APIs/#/Beacon/getEpochCommittees""" try: @@ -148,10 +140,9 @@ def get_attestation_committees( self.API_GET_ATTESTATION_COMMITTEES, path_params=(blockstamp.state_root,), query_params={'epoch': epoch, 'index': committee_index, 'slot': slot}, - force_raise=self.__raise_on_prysm_error + force_raise=self.__raise_on_prysm_error, + is_list=True, ) - if not isinstance(data, list): - raise ValueError("Expected list response from getEpochCommittees") except NotOkResponse as error: if self.PRYSM_STATE_NOT_FOUND_ERROR in error.text: data = self._get_attestation_committees_with_prysm( @@ -171,18 +162,19 @@ def get_sync_committee(self, blockstamp: BlockStamp, epoch: EpochNumber) -> Sync path_params=(blockstamp.state_root,), query_params={'epoch': epoch}, force_raise=self.__raise_on_prysm_error, + is_dict=True, ) - if not isinstance(data, dict): - raise ValueError("Expected mapping response from getSyncCommittees") return SyncCommittee.from_response(**data) @list_of_dataclasses(ProposerDuties.from_response) def get_proposer_duties(self, epoch: EpochNumber, expected_dependent_root: BlockRoot) -> list[ProposerDuties]: """Spec: https://ethereum.github.io/beacon-APIs/#/Validator/getProposerDuties""" # It is recommended by spec to use the dependent root to ensure the epoch is correct - proposer_data, proposer_meta = self._get(self.API_GET_PROPOSER_DUTIES, path_params=(epoch,)) - if not isinstance(proposer_data, list): - raise ValueError("Expected list response from getProposerDuties") + proposer_data, proposer_meta = self._get( + self.API_GET_PROPOSER_DUTIES, + path_params=(epoch,), + is_list=True, + ) response_dependent_root = proposer_meta['dependent_root'] if response_dependent_root != expected_dependent_root: raise ValueError( @@ -193,11 +185,12 @@ def get_proposer_duties(self, epoch: EpochNumber, expected_dependent_root: Block @lru_cache(maxsize=1) def get_state_block_roots(self, state_id: SlotNumber) -> list[BlockRoot]: - streamed_json = cast(TransientStreamingJSONObject, self._get( + streamed_json = self._get( self.API_GET_STATE, path_params=(state_id,), stream=True, - )) + is_dict=True, + ) return list(streamed_json['data']['block_roots']) def get_validators(self, blockstamp: BlockStamp) -> list[Validator]: @@ -238,11 +231,9 @@ def _get_state_by_state_id(self, state_id: StateRoot | SlotNumber) -> dict: data, _ = self._get( self.API_GET_STATE, path_params=(state_id,), - stream=False, force_raise=self.__raise_on_prysm_error, + is_dict=True, ) - if not isinstance(data, dict): - raise ValueError("Expected mapping response from getStateV2") return data def __raise_on_prysm_error(self, errors: list[Exception]) -> Exception | None: @@ -261,7 +252,7 @@ def _get_attestation_committees_with_prysm( blockstamp: BlockStamp, epoch: EpochNumber | None = None, index: int | None = None, - slot: SlotNumber | None = None + slot: SlotNumber | None = None, ) -> list[dict]: # Avoid Prysm issue with state root - https://github.com/prysmaticlabs/prysm/issues/12053 # Trying to get committees by slot number @@ -269,9 +260,8 @@ def _get_attestation_committees_with_prysm( self.API_GET_ATTESTATION_COMMITTEES, path_params=(blockstamp.slot_number,), query_params={'epoch': epoch, 'index': index, 'slot': slot}, + is_list=True, ) - if not isinstance(data, list): - raise ValueError("Expected list response from getEpochCommittees") return data def __raise_last_missed_slot_error(self, errors: list[Exception]) -> Exception | None: @@ -287,7 +277,9 @@ def __raise_last_missed_slot_error(self, errors: list[Exception]) -> Exception | return None def _get_chain_id_with_provider(self, provider_index: int) -> int: - data, _ = self._get_without_fallbacks(self.hosts[provider_index], self.API_GET_SPEC) - if not isinstance(data, dict): - raise ValueError("Expected mapping response from getSpec") + data, _ = self._get_without_fallbacks( + self.hosts[provider_index], + self.API_GET_SPEC, + is_dict=True, + ) return BeaconSpecResponse.from_response(**data).DEPOSIT_CHAIN_ID diff --git a/src/providers/http_provider.py b/src/providers/http_provider.py index 0dfd38914..4a47b8223 100644 --- a/src/providers/http_provider.py +++ b/src/providers/http_provider.py @@ -1,14 +1,14 @@ import logging from abc import ABC from http import HTTPStatus -from typing import Sequence, Callable +from typing import Callable, Literal, Sequence, overload from urllib.parse import urljoin, urlparse # NOTE: Missing library stubs or py.typed marker. That's why we use `type: ignore` from json_stream import requests as json_stream_requests # type: ignore from json_stream.base import TransientStreamingJSONList, TransientStreamingJSONObject # type: ignore from prometheus_client import Histogram -from requests import Session, JSONDecodeError +from requests import JSONDecodeError, Session from requests.adapters import HTTPAdapter from urllib3 import Retry @@ -35,6 +35,7 @@ class HTTPProvider(ProviderConsistencyModule, ABC): """ Base HTTP Provider with metrics and retry strategy integrated inside. """ + PROMETHEUS_HISTOGRAM: Histogram request_timeout: int @@ -72,6 +73,62 @@ def _urljoin(host, url): host += '/' return urljoin(host, url) + @overload + def _get( + self, + endpoint: str, + path_params: Sequence[str | int] | None = None, + query_params: dict | None = None, + *, + force_raise: Callable[..., Exception | None] = lambda _: None, + ) -> tuple[dict | list, dict]: ... + + @overload + def _get( + self, + endpoint: str, + path_params: Sequence[str | int] | None = None, + query_params: dict | None = None, + *, + force_raise: Callable[..., Exception | None] = lambda _: None, + is_dict: Literal[True], + ) -> tuple[dict, dict]: ... + + @overload + def _get( + self, + endpoint: str, + path_params: Sequence[str | int] | None = None, + query_params: dict | None = None, + *, + force_raise: Callable[..., Exception | None] = lambda _: None, + is_list: Literal[True], + ) -> tuple[list, dict]: ... + + @overload + def _get( + self, + endpoint: str, + path_params: Sequence[str | int] | None = None, + query_params: dict | None = None, + *, + force_raise: Callable[..., Exception | None] = lambda _: None, + stream: Literal[True], + is_dict: Literal[True], + ) -> TransientStreamingJSONObject: ... + + @overload + def _get( + self, + endpoint: str, + path_params: Sequence[str | int] | None = None, + query_params: dict | None = None, + *, + force_raise: Callable[..., Exception | None] = lambda _: None, + stream: Literal[True], + is_list: Literal[True], + ) -> TransientStreamingJSONList: ... + def _get( self, endpoint: str, @@ -79,7 +136,9 @@ def _get( query_params: dict | None = None, force_raise: Callable[..., Exception | None] = lambda _: None, stream: bool = False, - ) -> tuple[dict | list, dict] | TransientStreamingJSONObject | TransientStreamingJSONList: + is_dict: bool = False, + is_list: bool = False, + ): """ Get plain or streamed request with fallbacks Returns (data, meta) or raises exception @@ -91,7 +150,15 @@ def _get( for host in self.hosts: try: - return self._get_without_fallbacks(host, endpoint, path_params, query_params, stream) + return self._get_without_fallbacks( + host, + endpoint, + path_params, + query_params, + stream=stream, + is_dict=is_dict, + is_list=is_list, + ) except Exception as e: # pylint: disable=W0703 errors.append(e) @@ -110,14 +177,75 @@ def _get( # Raise error from last provider. raise errors[-1] + @overload def _get_without_fallbacks( self, host: str, endpoint: str, path_params: Sequence[str | int] | None = None, query_params: dict | None = None, + *, + is_dict: Literal[True], + ) -> tuple[dict, dict]: ... + + @overload + def _get_without_fallbacks( + self, + host: str, + endpoint: str, + path_params: Sequence[str | int] | None = None, + query_params: dict | None = None, + *, + is_list: Literal[True], + ) -> tuple[dict, dict]: ... + + @overload + def _get_without_fallbacks( + self, + host: str, + endpoint: str, + path_params: Sequence[str | int] | None = None, + query_params: dict | None = None, + *, + stream: Literal[True], + is_dict: Literal[True], + ) -> TransientStreamingJSONObject: ... + + @overload + def _get_without_fallbacks( + self, + host: str, + endpoint: str, + path_params: Sequence[str | int] | None = None, + query_params: dict | None = None, + *, + stream: Literal[True], + is_list: Literal[True], + ) -> TransientStreamingJSONList: ... + + @overload + def _get_without_fallbacks( + self, + host: str, + endpoint: str, + path_params: Sequence[str | int] | None = None, + query_params: dict | None = None, + *, stream: bool = False, - ) -> tuple[dict | list, dict] | TransientStreamingJSONObject | TransientStreamingJSONList: + is_dict: bool = False, + is_list: bool = False, + ) -> tuple[dict | list, dict] | TransientStreamingJSONObject | TransientStreamingJSONList: ... + + def _get_without_fallbacks( + self, + host: str, + endpoint: str, + path_params: Sequence[str | int] | None = None, + query_params: dict | None = None, + stream: bool = False, + is_dict: bool = False, + is_list: bool = False, + ): """ Simple get request without fallbacks Returns (data, meta) or streamed transient list-like or dict-like object JSON or raises exception @@ -130,7 +258,7 @@ def _get_without_fallbacks( self._urljoin(host, complete_endpoint if path_params else endpoint), params=query_params, timeout=self.request_timeout, - stream=stream + stream=stream, ) except Exception as error: logger.error({'msg': str(error)}) @@ -156,7 +284,12 @@ def _get_without_fallbacks( raise self.PROVIDER_EXCEPTION(response_fail_msg, status=response.status_code, text=response.text) if stream: - return json_stream_requests.load(response) + data = json_stream_requests.load(response) + if is_dict and not isinstance(data, TransientStreamingJSONObject): + raise ValueError(f"Expected mapping response from {endpoint}") + if is_list and not isinstance(data, TransientStreamingJSONList): + raise ValueError(f"Expected list response from {endpoint}") + return data try: json_response = response.json() @@ -167,13 +300,20 @@ def _get_without_fallbacks( logger.debug({'msg': response_fail_msg}) raise self.PROVIDER_EXCEPTION(status=0, text='JSON decode error.') from error - if 'data' in json_response: + try: data = json_response['data'] del json_response['data'] meta = json_response - else: + except KeyError: data = json_response meta = {} + except Exception as e: + raise ValueError(f"Unexpected response from {endpoint}, resp={response}") from e + + if is_dict and not isinstance(data, dict): + raise ValueError(f"Expected mapping response from {endpoint}") + if is_list and not isinstance(data, list): + raise ValueError(f"Expected list response from {endpoint}") return data, meta diff --git a/src/providers/keys/client.py b/src/providers/keys/client.py index ae0ba65a6..df10cf031 100644 --- a/src/providers/keys/client.py +++ b/src/providers/keys/client.py @@ -1,9 +1,9 @@ from time import sleep -from typing import cast, TypedDict, List +from typing import List, TypedDict, cast -from src.metrics.prometheus.basic import KEYS_API_REQUESTS_DURATION, KEYS_API_LATEST_BLOCKNUMBER +from src.metrics.prometheus.basic import KEYS_API_LATEST_BLOCKNUMBER, KEYS_API_REQUESTS_DURATION from src.providers.http_provider import HTTPProvider, NotOkResponse -from src.providers.keys.types import LidoKey, KeysApiStatus +from src.providers.keys.types import KeysApiStatus, LidoKey from src.types import BlockStamp, StakingModuleAddress from src.utils.cache import global_lru_cache as lru_cache @@ -32,6 +32,7 @@ class KeysAPIClient(HTTPProvider): Keys API specification can be found here https://keys-api.lido.fi/api/static/index.html """ + PROMETHEUS_HISTOGRAM = KEYS_API_REQUESTS_DURATION PROVIDER_EXCEPTION = KAPIClientError @@ -53,15 +54,19 @@ def _get_with_blockstamp(self, url: str, blockstamp: BlockStamp, params: dict | if i != self.retry_count - 1: sleep(self.backoff_factor) - raise KeysOutdatedException(f'Keys API Service stuck, no updates for {self.backoff_factor * self.retry_count} seconds.') + raise KeysOutdatedException( + f'Keys API Service stuck, no updates for {self.backoff_factor * self.retry_count} seconds.' + ) @lru_cache(maxsize=1) def get_used_lido_keys(self, blockstamp: BlockStamp) -> list[LidoKey]: """Docs: https://keys-api.lido.fi/api/static/index.html#/keys/KeysController_get""" - return list(map(lambda x: LidoKey.from_response(**x), self._get_with_blockstamp(self.USED_KEYS, blockstamp))) + return [LidoKey.from_response(**x) for x in self._get_with_blockstamp(self.USED_KEYS, blockstamp)] @lru_cache(maxsize=1) - def get_module_operators_keys(self, module_address: StakingModuleAddress, blockstamp: BlockStamp) -> ModuleOperatorsKeys: + def get_module_operators_keys( + self, module_address: StakingModuleAddress, blockstamp: BlockStamp + ) -> ModuleOperatorsKeys: """ Docs: https://keys-api.lido.fi/api/static/index.html#/operators-keys/SRModulesOperatorsKeysController_getOperatorsKeys """ @@ -71,9 +76,9 @@ def get_module_operators_keys(self, module_address: StakingModuleAddress, blocks def get_status(self) -> KeysApiStatus: """Docs: https://keys-api.lido.fi/api/static/index.html#/status/StatusController_get""" - data, _ = self._get(self.STATUS) - return KeysApiStatus.from_response(**cast(dict, data)) + data, _ = self._get(self.STATUS, is_dict=True) + return KeysApiStatus.from_response(**data) def _get_chain_id_with_provider(self, provider_index: int) -> int: - data, _ = self._get_without_fallbacks(self.hosts[provider_index], self.STATUS) - return KeysApiStatus.from_response(**cast(dict, data)).chainId + data, _ = self._get_without_fallbacks(self.hosts[provider_index], self.STATUS, is_dict=True) + return KeysApiStatus.from_response(**data).chainId diff --git a/tests/providers/consensus/test_consensus_client.py b/tests/providers/consensus/test_consensus_client.py index f29e48039..91cbaf3ec 100644 --- a/tests/providers/consensus/test_consensus_client.py +++ b/tests/providers/consensus/test_consensus_client.py @@ -1,14 +1,16 @@ # pylint: disable=protected-access """Simple tests for the consensus client responses validity.""" + from unittest.mock import Mock import pytest +import requests +from src import variables from src.providers.consensus.client import ConsensusClient from src.providers.consensus.types import Validator from src.types import SlotNumber from src.utils.blockstamp import build_blockstamp -from src import variables from tests.factory.blockstamp import BlockStampFactory @@ -94,7 +96,11 @@ def test_get_state_view(consensus_client: ConsensusClient): @pytest.mark.unit def test_get_returns_nor_dict_nor_list(consensus_client: ConsensusClient): - consensus_client._get_without_fallbacks = Mock(return_value=(1, None)) + resp = requests.Response() + resp.status_code = 200 + resp._content = b'{"data": 1}' + + consensus_client.session.get = Mock(return_value=resp) bs = BlockStampFactory.build() raises = pytest.raises(ValueError, match='Expected (mapping|list) response') diff --git a/tests/providers_clients/test_http_provider.py b/tests/providers_clients/test_http_provider.py index 1129c0b81..8e0cd1f54 100644 --- a/tests/providers_clients/test_http_provider.py +++ b/tests/providers_clients/test_http_provider.py @@ -1,5 +1,5 @@ # pylint: disable=protected-access -from unittest.mock import Mock, MagicMock +from unittest.mock import MagicMock, Mock import pytest @@ -28,7 +28,7 @@ def test_no_providers(): @pytest.mark.unit def test_all_fallbacks_ok(): provider = HTTPProvider(['http://localhost:1', 'http://localhost:2'], 5 * 60, 1, 1) - provider._get_without_fallbacks = lambda host, endpoint, path_params, query_params, stream: (host, endpoint) + provider._get_without_fallbacks = lambda host, endpoint, path_params, query_params, stream, **_: (host, endpoint) assert provider._get('test') == ('http://localhost:1', 'test') assert len(provider.get_all_providers()) == 2 @@ -42,7 +42,7 @@ def test_all_fallbacks_bad(): @pytest.mark.unit def test_first_fallback_bad(): - def _simple_get(host, endpoint, *_): + def _simple_get(host, endpoint, *args, **kwargs): if host == 'http://localhost:1': raise Exception('Bad host') # pylint: disable=broad-exception-raised return host, endpoint @@ -57,7 +57,7 @@ def test_force_raise(): class CustomError(Exception): pass - def _simple_get(host, endpoint, *_): + def _simple_get(host, endpoint, *args, **kwargs): if host == 'http://localhost:1': raise Exception('Bad host') # pylint: disable=broad-exception-raised return host, endpoint @@ -66,7 +66,15 @@ def _simple_get(host, endpoint, *_): provider._get_without_fallbacks = Mock(side_effect=_simple_get) with pytest.raises(CustomError): provider._get('test', force_raise=lambda _: CustomError()) - provider._get_without_fallbacks.assert_called_once_with('http://localhost:1', 'test', None, None, False) + provider._get_without_fallbacks.assert_called_once_with( + 'http://localhost:1', + 'test', + None, + None, + stream=False, + is_dict=False, + is_list=False, + ) @pytest.mark.unit From 7262c99e48ca6e8104e65d718eae46e5a4c9273e Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Wed, 21 May 2025 19:53:19 +0200 Subject: [PATCH 141/162] chore: black tests --- tests/modules/csm/test_csm_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/modules/csm/test_csm_module.py b/tests/modules/csm/test_csm_module.py index 8b1bdfa93..c35cd8997 100644 --- a/tests/modules/csm/test_csm_module.py +++ b/tests/modules/csm/test_csm_module.py @@ -594,6 +594,7 @@ def test_execute_module_not_collected(module: CSOracle): ) assert execute_delay is ModuleExecuteDelay.NEXT_FINALIZED_EPOCH + @pytest.mark.unit def test_execute_module_skips_collecting_if_forward_compatible(module: CSOracle): module._check_compatability = Mock(return_value=False) @@ -777,7 +778,6 @@ def test_make_strikes_tree(module: CSOracle, param: StrikesTreeTestParam): class TestLastReport: - @pytest.mark.unit def test_load(self, web3: Web3): blockstamp = Mock() From 6953e11f473a9d9bd69bbc8d26e560fd6ee41937 Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Thu, 22 May 2025 11:46:06 +0200 Subject: [PATCH 142/162] refactor: use explicit validator for http provider --- src/providers/consensus/client.py | 62 +++--- src/providers/http_provider.py | 183 ++++-------------- src/providers/keys/client.py | 6 +- .../consensus/test_consensus_client.py | 32 ++- tests/providers_clients/test_http_provider.py | 5 +- 5 files changed, 113 insertions(+), 175 deletions(-) diff --git a/src/providers/consensus/client.py b/src/providers/consensus/client.py index 46935da2c..b2dc66b79 100644 --- a/src/providers/consensus/client.py +++ b/src/providers/consensus/client.py @@ -1,5 +1,5 @@ from http import HTTPStatus -from typing import Literal, cast +from typing import Any, Literal, cast from src import variables from src.metrics.logging import logging @@ -20,7 +20,13 @@ SyncCommittee, Validator, ) -from src.providers.http_provider import HTTPProvider, NotOkResponse +from src.providers.http_provider import ( + HTTPProvider, + NotOkResponse, + data_is_dict, + data_is_list, + data_is_transient_dict, +) from src.types import BlockRoot, BlockStamp, EpochNumber, SlotNumber, StateRoot from src.utils.cache import global_lru_cache as lru_cache from src.utils.dataclass import list_of_dataclasses @@ -59,14 +65,14 @@ class ConsensusClient(HTTPProvider): def get_config_spec(self) -> BeaconSpecResponse: """Spec: https://ethereum.github.io/beacon-APIs/#/Config/getSpec""" - data, _ = self._get(self.API_GET_SPEC, is_dict=True) + data, _ = self._get(self.API_GET_SPEC, retval_validator=data_is_dict) return BeaconSpecResponse.from_response(**data) def get_genesis(self): """ Spec: https://ethereum.github.io/beacon-APIs/#/Beacon/getGenesis """ - data, _ = self._get(self.API_GET_GENESIS, is_dict=True) + data, _ = self._get(self.API_GET_GENESIS, retval_validator=data_is_dict) return GenesisResponse.from_response(**data) def get_block_root(self, state_id: SlotNumber | BlockRoot | LiteralState) -> BlockRootResponse: @@ -79,7 +85,7 @@ def get_block_root(self, state_id: SlotNumber | BlockRoot | LiteralState) -> Blo self.API_GET_BLOCK_ROOT, path_params=(state_id,), force_raise=self.__raise_last_missed_slot_error, - is_dict=True, + retval_validator=data_is_dict, ) return BlockRootResponse.from_response(**data) @@ -90,7 +96,7 @@ def get_block_header(self, state_id: SlotNumber | BlockRoot) -> BlockHeaderFullR self.API_GET_BLOCK_HEADER, path_params=(state_id,), force_raise=self.__raise_last_missed_slot_error, - is_dict=True, + retval_validator=data_is_dict, ) resp = BlockHeaderFullResponse.from_response(data=BlockHeaderResponseData.from_response(**data), **meta_data) return resp @@ -102,7 +108,7 @@ def get_block_details(self, state_id: SlotNumber | BlockRoot) -> BlockDetailsRes self.API_GET_BLOCK_DETAILS, path_params=(state_id,), force_raise=self.__raise_last_missed_slot_error, - is_dict=True, + retval_validator=data_is_dict, ) return BlockDetailsResponse.from_response(**data) @@ -115,7 +121,7 @@ def get_block_attestations_and_sync( self.API_GET_BLOCK_DETAILS, path_params=(state_id,), force_raise=self.__raise_last_missed_slot_error, - is_dict=True, + retval_validator=data_is_dict, ) attestations = [ @@ -141,7 +147,7 @@ def get_attestation_committees( path_params=(blockstamp.state_root,), query_params={'epoch': epoch, 'index': committee_index, 'slot': slot}, force_raise=self.__raise_on_prysm_error, - is_list=True, + retval_validator=data_is_list, ) except NotOkResponse as error: if self.PRYSM_STATE_NOT_FOUND_ERROR in error.text: @@ -162,36 +168,40 @@ def get_sync_committee(self, blockstamp: BlockStamp, epoch: EpochNumber) -> Sync path_params=(blockstamp.state_root,), query_params={'epoch': epoch}, force_raise=self.__raise_on_prysm_error, - is_dict=True, + retval_validator=data_is_dict, ) return SyncCommittee.from_response(**data) @list_of_dataclasses(ProposerDuties.from_response) def get_proposer_duties(self, epoch: EpochNumber, expected_dependent_root: BlockRoot) -> list[ProposerDuties]: """Spec: https://ethereum.github.io/beacon-APIs/#/Validator/getProposerDuties""" - # It is recommended by spec to use the dependent root to ensure the epoch is correct - proposer_data, proposer_meta = self._get( + + def data_is_list_and_dependent_root_matches(data: Any, meta: dict, endpoint: str): + data_is_list(data, meta, endpoint=endpoint) + # It is recommended by spec to use the dependent root to ensure the epoch is correct + if meta["dependent_root"] != expected_dependent_root: + raise ValueError( + "Dependent root for proposer duties request mismatch: " + f"{meta['dependent_root']=} is not {expected_dependent_root=}. " + "Probably, CL node is not fully synced." + ) + + data, _ = self._get( self.API_GET_PROPOSER_DUTIES, path_params=(epoch,), - is_list=True, + retval_validator=data_is_list_and_dependent_root_matches, ) - response_dependent_root = proposer_meta['dependent_root'] - if response_dependent_root != expected_dependent_root: - raise ValueError( - "Dependent root for proposer duties request mismatch: " - f"{response_dependent_root=} is not {expected_dependent_root=}. Probably, CL node is not fully synced" - ) - return proposer_data + return data @lru_cache(maxsize=1) def get_state_block_roots(self, state_id: SlotNumber) -> list[BlockRoot]: - streamed_json = self._get( + data, _ = self._get( self.API_GET_STATE, path_params=(state_id,), stream=True, - is_dict=True, + retval_validator=data_is_transient_dict, ) - return list(streamed_json['data']['block_roots']) + return list(data["block_roots"]) def get_validators(self, blockstamp: BlockStamp) -> list[Validator]: return self.get_state_view(blockstamp).indexed_validators @@ -232,7 +242,7 @@ def _get_state_by_state_id(self, state_id: StateRoot | SlotNumber) -> dict: self.API_GET_STATE, path_params=(state_id,), force_raise=self.__raise_on_prysm_error, - is_dict=True, + retval_validator=data_is_dict, ) return data @@ -260,7 +270,7 @@ def _get_attestation_committees_with_prysm( self.API_GET_ATTESTATION_COMMITTEES, path_params=(blockstamp.slot_number,), query_params={'epoch': epoch, 'index': index, 'slot': slot}, - is_list=True, + retval_validator=data_is_dict, ) return data @@ -280,6 +290,6 @@ def _get_chain_id_with_provider(self, provider_index: int) -> int: data, _ = self._get_without_fallbacks( self.hosts[provider_index], self.API_GET_SPEC, - is_dict=True, + retval_validator=data_is_dict, ) return BeaconSpecResponse.from_response(**data).DEPOSIT_CHAIN_ID diff --git a/src/providers/http_provider.py b/src/providers/http_provider.py index 4a47b8223..6ab543793 100644 --- a/src/providers/http_provider.py +++ b/src/providers/http_provider.py @@ -1,12 +1,12 @@ import logging from abc import ABC from http import HTTPStatus -from typing import Callable, Literal, Sequence, overload +from typing import Any, Callable, NoReturn, Protocol, Sequence from urllib.parse import urljoin, urlparse # NOTE: Missing library stubs or py.typed marker. That's why we use `type: ignore` from json_stream import requests as json_stream_requests # type: ignore -from json_stream.base import TransientStreamingJSONList, TransientStreamingJSONObject # type: ignore +from json_stream.base import TransientStreamingJSONObject # type: ignore from prometheus_client import Histogram from requests import JSONDecodeError, Session from requests.adapters import HTTPAdapter @@ -31,6 +31,29 @@ def __init__(self, *args, status: int, text: str): super().__init__(*args) +class ReturnValueValidator(Protocol): + def __call__(self, data: Any, meta: dict, *, endpoint: str) -> None | NoReturn: ... + + +def data_is_any(data: Any, meta: dict, *, endpoint: str): + pass + + +def data_is_dict(data: Any, meta: dict, *, endpoint: str): + if not isinstance(data, dict): + raise ValueError(f"Expected mapping response from {endpoint}") + + +def data_is_list(data: Any, meta: dict, *, endpoint: str): + if not isinstance(data, list): + raise ValueError(f"Expected list response from {endpoint}") + + +def data_is_transient_dict(data: Any, meta: dict, *, endpoint: str): + if not isinstance(data, TransientStreamingJSONObject): + raise ValueError(f"Expected mapping response from {endpoint}") + + class HTTPProvider(ProviderConsistencyModule, ABC): """ Base HTTP Provider with metrics and retry strategy integrated inside. @@ -73,72 +96,15 @@ def _urljoin(host, url): host += '/' return urljoin(host, url) - @overload - def _get( - self, - endpoint: str, - path_params: Sequence[str | int] | None = None, - query_params: dict | None = None, - *, - force_raise: Callable[..., Exception | None] = lambda _: None, - ) -> tuple[dict | list, dict]: ... - - @overload - def _get( - self, - endpoint: str, - path_params: Sequence[str | int] | None = None, - query_params: dict | None = None, - *, - force_raise: Callable[..., Exception | None] = lambda _: None, - is_dict: Literal[True], - ) -> tuple[dict, dict]: ... - - @overload - def _get( - self, - endpoint: str, - path_params: Sequence[str | int] | None = None, - query_params: dict | None = None, - *, - force_raise: Callable[..., Exception | None] = lambda _: None, - is_list: Literal[True], - ) -> tuple[list, dict]: ... - - @overload - def _get( - self, - endpoint: str, - path_params: Sequence[str | int] | None = None, - query_params: dict | None = None, - *, - force_raise: Callable[..., Exception | None] = lambda _: None, - stream: Literal[True], - is_dict: Literal[True], - ) -> TransientStreamingJSONObject: ... - - @overload - def _get( - self, - endpoint: str, - path_params: Sequence[str | int] | None = None, - query_params: dict | None = None, - *, - force_raise: Callable[..., Exception | None] = lambda _: None, - stream: Literal[True], - is_list: Literal[True], - ) -> TransientStreamingJSONList: ... - def _get( self, endpoint: str, path_params: Sequence[str | int] | None = None, query_params: dict | None = None, force_raise: Callable[..., Exception | None] = lambda _: None, + retval_validator: ReturnValueValidator = data_is_any, stream: bool = False, - is_dict: bool = False, - is_list: bool = False, - ): + ) -> tuple[Any, dict]: """ Get plain or streamed request with fallbacks Returns (data, meta) or raises exception @@ -156,8 +122,7 @@ def _get( path_params, query_params, stream=stream, - is_dict=is_dict, - is_list=is_list, + retval_validator=retval_validator, ) except Exception as e: # pylint: disable=W0703 errors.append(e) @@ -177,78 +142,18 @@ def _get( # Raise error from last provider. raise errors[-1] - @overload - def _get_without_fallbacks( - self, - host: str, - endpoint: str, - path_params: Sequence[str | int] | None = None, - query_params: dict | None = None, - *, - is_dict: Literal[True], - ) -> tuple[dict, dict]: ... - - @overload - def _get_without_fallbacks( - self, - host: str, - endpoint: str, - path_params: Sequence[str | int] | None = None, - query_params: dict | None = None, - *, - is_list: Literal[True], - ) -> tuple[dict, dict]: ... - - @overload def _get_without_fallbacks( self, host: str, endpoint: str, path_params: Sequence[str | int] | None = None, query_params: dict | None = None, - *, - stream: Literal[True], - is_dict: Literal[True], - ) -> TransientStreamingJSONObject: ... - - @overload - def _get_without_fallbacks( - self, - host: str, - endpoint: str, - path_params: Sequence[str | int] | None = None, - query_params: dict | None = None, - *, - stream: Literal[True], - is_list: Literal[True], - ) -> TransientStreamingJSONList: ... - - @overload - def _get_without_fallbacks( - self, - host: str, - endpoint: str, - path_params: Sequence[str | int] | None = None, - query_params: dict | None = None, - *, stream: bool = False, - is_dict: bool = False, - is_list: bool = False, - ) -> tuple[dict | list, dict] | TransientStreamingJSONObject | TransientStreamingJSONList: ... - - def _get_without_fallbacks( - self, - host: str, - endpoint: str, - path_params: Sequence[str | int] | None = None, - query_params: dict | None = None, - stream: bool = False, - is_dict: bool = False, - is_list: bool = False, - ): + retval_validator: ReturnValueValidator = data_is_any, + ) -> tuple[Any, dict]: """ Simple get request without fallbacks - Returns (data, meta) or streamed transient list-like or dict-like object JSON or raises exception + Returns (data, meta) or raises an exception """ complete_endpoint = endpoint.format(*path_params) if path_params else endpoint @@ -284,12 +189,8 @@ def _get_without_fallbacks( raise self.PROVIDER_EXCEPTION(response_fail_msg, status=response.status_code, text=response.text) if stream: - data = json_stream_requests.load(response) - if is_dict and not isinstance(data, TransientStreamingJSONObject): - raise ValueError(f"Expected mapping response from {endpoint}") - if is_list and not isinstance(data, TransientStreamingJSONList): - raise ValueError(f"Expected list response from {endpoint}") - return data + # There's no guarantee the JSON is valid at this point. + json_response = json_stream_requests.load(response) try: json_response = response.json() @@ -301,20 +202,18 @@ def _get_without_fallbacks( raise self.PROVIDER_EXCEPTION(status=0, text='JSON decode error.') from error try: - data = json_response['data'] - del json_response['data'] - meta = json_response + data = json_response["data"] + meta = {} + + if not stream: + del json_response["data"] + meta = json_response except KeyError: + # NOTE: Used by KeysAPIClient only. data = json_response meta = {} - except Exception as e: - raise ValueError(f"Unexpected response from {endpoint}, resp={response}") from e - - if is_dict and not isinstance(data, dict): - raise ValueError(f"Expected mapping response from {endpoint}") - if is_list and not isinstance(data, list): - raise ValueError(f"Expected list response from {endpoint}") + retval_validator(data, meta, endpoint=endpoint) return data, meta def get_all_providers(self) -> list[str]: diff --git a/src/providers/keys/client.py b/src/providers/keys/client.py index df10cf031..70252071b 100644 --- a/src/providers/keys/client.py +++ b/src/providers/keys/client.py @@ -2,7 +2,7 @@ from typing import List, TypedDict, cast from src.metrics.prometheus.basic import KEYS_API_LATEST_BLOCKNUMBER, KEYS_API_REQUESTS_DURATION -from src.providers.http_provider import HTTPProvider, NotOkResponse +from src.providers.http_provider import HTTPProvider, NotOkResponse, data_is_dict from src.providers.keys.types import KeysApiStatus, LidoKey from src.types import BlockStamp, StakingModuleAddress from src.utils.cache import global_lru_cache as lru_cache @@ -76,9 +76,9 @@ def get_module_operators_keys( def get_status(self) -> KeysApiStatus: """Docs: https://keys-api.lido.fi/api/static/index.html#/status/StatusController_get""" - data, _ = self._get(self.STATUS, is_dict=True) + data, _ = self._get(self.STATUS, retval_validator=data_is_dict) return KeysApiStatus.from_response(**data) def _get_chain_id_with_provider(self, provider_index: int) -> int: - data, _ = self._get_without_fallbacks(self.hosts[provider_index], self.STATUS, is_dict=True) + data, _ = self._get_without_fallbacks(self.hosts[provider_index], self.STATUS, retval_validator=data_is_dict) return KeysApiStatus.from_response(**data).chainId diff --git a/tests/providers/consensus/test_consensus_client.py b/tests/providers/consensus/test_consensus_client.py index 91cbaf3ec..87ee5a188 100644 --- a/tests/providers/consensus/test_consensus_client.py +++ b/tests/providers/consensus/test_consensus_client.py @@ -9,7 +9,7 @@ from src import variables from src.providers.consensus.client import ConsensusClient from src.providers.consensus.types import Validator -from src.types import SlotNumber +from src.types import EpochNumber, SlotNumber from src.utils.blockstamp import build_blockstamp from tests.factory.blockstamp import BlockStampFactory @@ -120,8 +120,38 @@ def test_get_returns_nor_dict_nor_list(consensus_client: ConsensusClient): with raises: consensus_client.get_block_details(SlotNumber(0)) + with raises: + consensus_client.get_block_attestations_and_sync(SlotNumber(0)) + + with raises: + consensus_client.get_attestation_committees(bs) + + with raises: + consensus_client.get_sync_committee(bs, EpochNumber(0)) + + with raises: + consensus_client.get_proposer_duties(EpochNumber(0), Mock()) + + with raises: + consensus_client.get_state_block_roots(SlotNumber(0)) + + with raises: + consensus_client.get_state_view_no_cache(bs) + with raises: consensus_client.get_validators_no_cache(bs) with raises: consensus_client._get_chain_id_with_provider(0) + + +@pytest.mark.unit +def test_get_proposer_duties_fails_on_root_check(consensus_client: ConsensusClient): + resp = requests.Response() + resp.status_code = 200 + resp._content = b'{"data": [], "dependent_root": "0x01"}' + + consensus_client.session.get = Mock(return_value=resp) + + with pytest.raises(ValueError, match="Dependent root for proposer duties request mismatch"): + consensus_client.get_proposer_duties(EpochNumber(0), "0x02") diff --git a/tests/providers_clients/test_http_provider.py b/tests/providers_clients/test_http_provider.py index 8e0cd1f54..67c3928a8 100644 --- a/tests/providers_clients/test_http_provider.py +++ b/tests/providers_clients/test_http_provider.py @@ -3,7 +3,7 @@ import pytest -from src.providers.http_provider import HTTPProvider, NoHostsProvided, NotOkResponse +from src.providers.http_provider import HTTPProvider, NoHostsProvided, NotOkResponse, data_is_any @pytest.mark.unit @@ -72,8 +72,7 @@ def _simple_get(host, endpoint, *args, **kwargs): None, None, stream=False, - is_dict=False, - is_list=False, + retval_validator=data_is_any, ) From dd942d6782de777e51caa2940f2edbeec1f4ed6e Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Thu, 22 May 2025 12:33:51 +0200 Subject: [PATCH 143/162] test: add retval_validator simple test --- tests/providers_clients/test_http_provider.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/providers_clients/test_http_provider.py b/tests/providers_clients/test_http_provider.py index 67c3928a8..b797f0434 100644 --- a/tests/providers_clients/test_http_provider.py +++ b/tests/providers_clients/test_http_provider.py @@ -2,7 +2,9 @@ from unittest.mock import MagicMock, Mock import pytest +from requests import Response +from src.metrics.prometheus.basic import CL_REQUESTS_DURATION from src.providers.http_provider import HTTPProvider, NoHostsProvided, NotOkResponse, data_is_any @@ -76,6 +78,23 @@ def _simple_get(host, endpoint, *args, **kwargs): ) +@pytest.mark.unit +def test_retval_validator(): + provider = HTTPProvider(['http://localhost:1', 'http://localhost:2'], 5 * 60, 1, 1) + provider.PROMETHEUS_HISTOGRAM = CL_REQUESTS_DURATION + + resp = Response() + resp.status_code = 200 + resp._content = b'{"data": {}}' + provider.session.get = Mock(return_value=resp) + + def failed_validation(*args, **kwargs): + raise ValueError("Validation failed") + + with pytest.raises(ValueError, match="Validation failed"): + provider._get('test', retval_validator=failed_validation) + + @pytest.mark.unit def test_custom_error_provided(): class CustomError(NotOkResponse): From e78082fc8006e80ebb798da009a340aeddc7b724 Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Thu, 22 May 2025 12:40:36 +0200 Subject: [PATCH 144/162] chore: remove unused checks in last_report --- src/modules/csm/helpers/last_report.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/modules/csm/helpers/last_report.py b/src/modules/csm/helpers/last_report.py index 7c57b054d..c8500a0fc 100644 --- a/src/modules/csm/helpers/last_report.py +++ b/src/modules/csm/helpers/last_report.py @@ -51,12 +51,6 @@ def load(cls, w3: Web3, blockstamp: BlockStamp) -> Self: @cached_property def rewards(self) -> Iterable[RewardsTreeLeaf]: - if (self.rewards_tree_cid is None) != (self.rewards_tree_root == ZERO_HASH): - raise InconsistentData( - "Got inconsistent previous rewards tree data: " - f"tree_root={self.rewards_tree_root.to_0x_hex()} tree_cid={self.rewards_tree_cid=}" - ) - if self.rewards_tree_cid is None or self.rewards_tree_root == ZERO_HASH: logger.info({"msg": f"No rewards distribution as of {self.blockstamp=}."}) return [] @@ -73,12 +67,6 @@ def rewards(self) -> Iterable[RewardsTreeLeaf]: @cached_property def strikes(self) -> dict[StrikesValidator, StrikesList]: - if (self.strikes_tree_cid is None) != (self.strikes_tree_root == ZERO_HASH): - raise InconsistentData( - "Got inconsistent previous strikes tree data: " - f"tree_root={self.strikes_tree_root.to_0x_hex()} tree_cid={self.strikes_tree_cid=}" - ) - if self.strikes_tree_cid is None or self.strikes_tree_root == ZERO_HASH: logger.info({"msg": f"No strikes reported as of {self.blockstamp=}."}) return {} From 6f1946c2caa7ce1e8efae28cf88c96be4b97e686 Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Thu, 22 May 2025 12:41:13 +0200 Subject: [PATCH 145/162] chore: wrap tree creation error with ValueError --- src/modules/csm/tree.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/modules/csm/tree.py b/src/modules/csm/tree.py index 03732e209..92abb5098 100644 --- a/src/modules/csm/tree.py +++ b/src/modules/csm/tree.py @@ -46,7 +46,9 @@ def decode(cls, content: bytes) -> Self: try: return cls(StandardMerkleTree.load(json.loads(content, cls=cls.decoder))) except JSONDecodeError as e: - raise ValueError("Unsupported tree format") from e + raise ValueError("Invalid tree's JSON") from e + except Exception as e: + raise ValueError("Unable to load tree") from e def encode(self) -> bytes: """Convert the underlying StandardMerkleTree to a binary representation""" From c3626b56edb2de4601d87d16a74ac3bbb76d905d Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Thu, 22 May 2025 12:46:04 +0200 Subject: [PATCH 146/162] chore: mark cs_module check with pytest.mark.mainnet --- tests/integration/contracts/test_cs_module.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/integration/contracts/test_cs_module.py b/tests/integration/contracts/test_cs_module.py index 7b726d6ee..567189b28 100644 --- a/tests/integration/contracts/test_cs_module.py +++ b/tests/integration/contracts/test_cs_module.py @@ -6,6 +6,7 @@ @pytest.mark.integration +@pytest.mark.mainnet def test_cs_module(cs_module_contract, caplog): check_contract( cs_module_contract, @@ -18,6 +19,7 @@ def test_cs_module(cs_module_contract, caplog): @pytest.mark.integration +@pytest.mark.mainnet @pytest.mark.xfail(raises=ContractLogicError, reason="CSMv2 is not yet live") def test_cs_module_v2(cs_module_contract, caplog): check_contract( From c7e2b5e3d6335ab006b9a5563cf2f639736f8a7b Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Thu, 22 May 2025 12:50:30 +0200 Subject: [PATCH 147/162] chore: move distributed_so_far update --- src/modules/csm/distribution.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/modules/csm/distribution.py b/src/modules/csm/distribution.py index 42ce33459..c2899ae95 100644 --- a/src/modules/csm/distribution.py +++ b/src/modules/csm/distribution.py @@ -50,6 +50,7 @@ def calculate(self, blockstamp: ReferenceBlockStamp, last_report: LastReport) -> result = DistributionResult() result.strikes.update(last_report.strikes.items()) + distributed_so_far = 0 for frame in self.state.frames: from_epoch, to_epoch = frame logger.info({"msg": f"Calculating distribution for frame [{from_epoch};{to_epoch}]"}) @@ -58,7 +59,6 @@ def calculate(self, blockstamp: ReferenceBlockStamp, last_report: LastReport) -> frame_module_validators = self._get_module_validators(frame_blockstamp) total_rewards_to_distribute = self.w3.csm.fee_distributor.shares_to_distribute(frame_blockstamp.block_hash) - distributed_so_far = result.total_rewards + result.total_rebate rewards_to_distribute_in_frame = total_rewards_to_distribute - distributed_so_far frame_log = FramePerfLog(frame_blockstamp, frame) @@ -78,6 +78,7 @@ def calculate(self, blockstamp: ReferenceBlockStamp, last_report: LastReport) -> result.total_rebate += rebate_to_protocol_in_frame self.validate_distribution(result.total_rewards, result.total_rebate, total_rewards_to_distribute) + distributed_so_far = result.total_rewards + result.total_rebate for no_id, rewards in rewards_map_in_frame.items(): result.total_rewards_map[no_id] += rewards From 573d810eb6817a7290b85823f628d8aa4228ccd5 Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Thu, 22 May 2025 13:07:26 +0200 Subject: [PATCH 148/162] refactor: make frames a property --- src/modules/csm/state.py | 10 +++--- tests/modules/csm/test_csm_distribution.py | 6 ++-- tests/modules/csm/test_state.py | 38 ++++------------------ 3 files changed, 16 insertions(+), 38 deletions(-) diff --git a/src/modules/csm/state.py b/src/modules/csm/state.py index 974437589..6076b3b15 100644 --- a/src/modules/csm/state.py +++ b/src/modules/csm/state.py @@ -67,6 +67,8 @@ def merge(self, other: Self) -> None: class State: + # pylint: disable=too-many-public-methods + """ Processing state of a CSM performance oracle frame. @@ -77,7 +79,6 @@ class State: The state can be migrated to be used for another frame's report by calling the `migrate` method. """ - frames: list[Frame] data: StateData _epochs_to_process: tuple[EpochNumber, ...] @@ -86,7 +87,6 @@ class State: _consensus_version: int def __init__(self) -> None: - self.frames = [] self.data = {} self._epochs_to_process = tuple() self._processed_epochs = set() @@ -130,6 +130,10 @@ def buffer(self) -> Path: def is_empty(self) -> bool: return not self.data and not self._epochs_to_process and not self._processed_epochs + @property + def frames(self) -> list[Frame]: + return list(self.data.keys()) + @property def unprocessed_epochs(self) -> set[EpochNumber]: if not self._epochs_to_process: @@ -149,7 +153,6 @@ def _calculate_frames(epochs_to_process: tuple[EpochNumber, ...], epochs_per_fra return [(frame[0], frame[-1]) for frame in batched(sorted(epochs_to_process), epochs_per_frame)] def clear(self) -> None: - self.frames = [] self.data = {} self._epochs_to_process = tuple() self._processed_epochs.clear() @@ -200,7 +203,6 @@ def migrate( return self._migrate_frames_data(new_frames) - self.frames = new_frames self.find_frame.cache_clear() self._epochs_to_process = tuple(sequence(l_epoch, r_epoch)) self._consensus_version = consensus_version diff --git a/tests/modules/csm/test_csm_distribution.py b/tests/modules/csm/test_csm_distribution.py index b33c93f96..c5340c129 100644 --- a/tests/modules/csm/test_csm_distribution.py +++ b/tests/modules/csm/test_csm_distribution.py @@ -298,7 +298,7 @@ def test_calculate_distribution( distribution = Distribution(w3, converter=..., state=State()) distribution._get_module_validators = Mock(...) - distribution.state.frames = frames + distribution.state.data = {f: {} for f in frames} distribution._get_frame_blockstamp = Mock(side_effect=frame_blockstamps) distribution._calculate_distribution_in_frame = Mock(side_effect=distribution_in_frame) @@ -324,7 +324,7 @@ def test_calculate_distribution_handles_invalid_distribution(): distribution = Distribution(w3, converter=..., state=State()) distribution._get_module_validators = Mock(...) - distribution.state.frames = [(EpochNumber(0), EpochNumber(31))] + distribution.state.data = {(EpochNumber(0), EpochNumber(31)): {}} distribution._get_frame_blockstamp = Mock(return_value=ReferenceBlockStampFactory.build(ref_epoch=31)) distribution._calculate_distribution_in_frame = Mock( return_value=( @@ -352,7 +352,7 @@ def test_calculate_distribution_handles_invalid_distribution_in_total(): distribution = Distribution(w3, converter=..., state=State()) distribution._get_module_validators = Mock(...) - distribution.state.frames = [(EpochNumber(0), EpochNumber(31))] + distribution.state.data = {(EpochNumber(0), EpochNumber(31)): {}} distribution._get_frame_blockstamp = Mock(return_value=ReferenceBlockStampFactory.build(ref_epoch=31)) distribution._calculate_distribution_in_frame = Mock( return_value=( diff --git a/tests/modules/csm/test_state.py b/tests/modules/csm/test_state.py index 26127842b..9f59f43b0 100644 --- a/tests/modules/csm/test_state.py +++ b/tests/modules/csm/test_state.py @@ -154,14 +154,14 @@ def test_clear_resets_state_to_empty(): @pytest.mark.unit def test_find_frame_returns_correct_frame(): state = State() - state.frames = [(0, 31)] + state.data = {(0, 31): {}} assert state.find_frame(15) == (0, 31) @pytest.mark.unit def test_find_frame_raises_error_for_out_of_range_epoch(): state = State() - state.frames = [(0, 31)] + state.data = {(0, 31): {}} with pytest.raises(ValueError, match="Epoch 32 is out of frames range"): state.find_frame(32) @@ -170,7 +170,6 @@ def test_find_frame_raises_error_for_out_of_range_epoch(): def test_increment_att_duty_adds_duty_correctly(): state = State() frame = (0, 31) - state.frames = [frame] duty_epoch, _ = frame state.data = { frame: NetworkDuties(attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})), @@ -184,7 +183,6 @@ def test_increment_att_duty_adds_duty_correctly(): def test_increment_prop_duty_adds_duty_correctly(): state = State() frame = (0, 31) - state.frames = [frame] duty_epoch, _ = frame state.data = { frame: NetworkDuties(proposals=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})), @@ -198,7 +196,6 @@ def test_increment_prop_duty_adds_duty_correctly(): def test_increment_sync_duty_adds_duty_correctly(): state = State() frame = (0, 31) - state.frames = [frame] duty_epoch, _ = frame state.data = { frame: NetworkDuties(syncs=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})), @@ -212,7 +209,6 @@ def test_increment_sync_duty_adds_duty_correctly(): def test_increment_att_duty_creates_new_validator_entry(): state = State() frame = (0, 31) - state.frames = [frame] duty_epoch, _ = frame state.data = { frame: NetworkDuties(), @@ -226,7 +222,6 @@ def test_increment_att_duty_creates_new_validator_entry(): def test_increment_prop_duty_creates_new_validator_entry(): state = State() frame = (0, 31) - state.frames = [frame] duty_epoch, _ = frame state.data = { frame: NetworkDuties(), @@ -240,7 +235,6 @@ def test_increment_prop_duty_creates_new_validator_entry(): def test_increment_sync_duty_creates_new_validator_entry(): state = State() frame = (0, 31) - state.frames = [frame] duty_epoch, _ = frame state.data = { frame: NetworkDuties(), @@ -254,7 +248,6 @@ def test_increment_sync_duty_creates_new_validator_entry(): def test_increment_att_duty_handles_non_included_duty(): state = State() frame = (0, 31) - state.frames = [frame] duty_epoch, _ = frame state.data = { frame: NetworkDuties(attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})), @@ -268,7 +261,6 @@ def test_increment_att_duty_handles_non_included_duty(): def test_increment_prop_duty_handles_non_included_duty(): state = State() frame = (0, 31) - state.frames = [frame] duty_epoch, _ = frame state.data = { frame: NetworkDuties(proposals=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})), @@ -282,7 +274,6 @@ def test_increment_prop_duty_handles_non_included_duty(): def test_increment_sync_duty_handles_non_included_duty(): state = State() frame = (0, 31) - state.frames = [frame] duty_epoch, _ = frame state.data = { frame: NetworkDuties(syncs=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)})), @@ -295,8 +286,6 @@ def test_increment_sync_duty_handles_non_included_duty(): @pytest.mark.unit def test_increment_att_duty_raises_error_for_out_of_range_epoch(): state = State() - frame = (0, 31) - state.frames = [frame] state.att_data = { (0, 31): defaultdict(DutyAccumulator), } @@ -307,8 +296,6 @@ def test_increment_att_duty_raises_error_for_out_of_range_epoch(): @pytest.mark.unit def test_increment_prop_duty_raises_error_for_out_of_range_epoch(): state = State() - frame = (0, 31) - state.frames = [frame] state.att_data = { (0, 31): defaultdict(DutyAccumulator), } @@ -319,8 +306,6 @@ def test_increment_prop_duty_raises_error_for_out_of_range_epoch(): @pytest.mark.unit def test_increment_sync_duty_raises_error_for_out_of_range_epoch(): state = State() - frame = (0, 31) - state.frames = [frame] state.att_data = { (0, 31): defaultdict(DutyAccumulator), } @@ -362,7 +347,6 @@ def test_migrate_discards_data_on_version_change(): def test_migrate_no_migration_needed(): state = State() state._consensus_version = 1 - state.frames = [(0, 31), (32, 63)] state.data = { (0, 31): defaultdict(DutyAccumulator), (32, 63): defaultdict(DutyAccumulator), @@ -381,7 +365,6 @@ def test_migrate_no_migration_needed(): def test_migrate_migrates_data(): state = State() state._consensus_version = 1 - state.frames = [(0, 31), (32, 63)] state.data = { (0, 31): NetworkDuties( attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), @@ -414,7 +397,6 @@ def test_migrate_migrates_data(): def test_migrate_invalidates_unmigrated_frames(): state = State() state._consensus_version = 1 - state.frames = [(0, 63)] state.data = { (0, 63): NetworkDuties( attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(30, 20)}), @@ -439,7 +421,6 @@ def test_migrate_invalidates_unmigrated_frames(): def test_migrate_discards_unmigrated_frame(): state = State() state._consensus_version = 1 - state.frames = [(0, 31), (32, 63), (64, 95)] state.data = { (0, 31): NetworkDuties( attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), @@ -483,8 +464,6 @@ def test_migrate_discards_unmigrated_frame(): @pytest.mark.unit def test_migrate_frames_data_creates_new_data_correctly(): state = State() - state.frames = [(0, 31), (32, 63)] - new_frames = [(0, 63)] state.data = { (0, 31): NetworkDuties( attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), @@ -499,6 +478,7 @@ def test_migrate_frames_data_creates_new_data_correctly(): } state._processed_epochs = set(sequence(0, 20)) + new_frames = [(0, 63)] state._migrate_frames_data(new_frames) assert state.data == { @@ -514,8 +494,6 @@ def test_migrate_frames_data_creates_new_data_correctly(): @pytest.mark.unit def test_migrate_frames_data_handles_no_migration(): state = State() - state.frames = [(0, 31)] - new_frames = [(0, 31)] state.data = { (0, 31): NetworkDuties( attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), @@ -525,6 +503,7 @@ def test_migrate_frames_data_handles_no_migration(): } state._processed_epochs = set(sequence(0, 20)) + new_frames = [(0, 31)] state._migrate_frames_data(new_frames) assert state.data == { @@ -540,8 +519,6 @@ def test_migrate_frames_data_handles_no_migration(): @pytest.mark.unit def test_migrate_frames_data_handles_partial_migration(): state = State() - state.frames = [(0, 31), (32, 63)] - new_frames = [(0, 31), (32, 95)] state.data = { (0, 31): NetworkDuties( attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(10, 5)}), @@ -556,6 +533,7 @@ def test_migrate_frames_data_handles_partial_migration(): } state._processed_epochs = set(sequence(0, 20)) + new_frames = [(0, 31), (32, 95)] state._migrate_frames_data(new_frames) assert state.data == { @@ -576,10 +554,9 @@ def test_migrate_frames_data_handles_partial_migration(): @pytest.mark.unit def test_migrate_frames_data_handles_no_data(): state = State() - state.frames = [(0, 31)] - new_frames = [(0, 31)] state.data = {frame: NetworkDuties() for frame in state.frames} + new_frames = [(0, 31)] state._migrate_frames_data(new_frames) assert state.data == {(0, 31): NetworkDuties()} @@ -588,8 +565,6 @@ def test_migrate_frames_data_handles_no_data(): @pytest.mark.unit def test_migrate_frames_data_handles_wider_old_frame(): state = State() - state.frames = [(0, 63)] - new_frames = [(0, 31), (32, 63)] state.data = { (0, 63): NetworkDuties( attestations=defaultdict(DutyAccumulator, {ValidatorIndex(1): DutyAccumulator(30, 20)}), @@ -599,6 +574,7 @@ def test_migrate_frames_data_handles_wider_old_frame(): } state._processed_epochs = set(sequence(0, 20)) + new_frames = [(0, 31), (32, 63)] state._migrate_frames_data(new_frames) assert state.data == { From b9f3364461ca4ccdb889f1e245c2513c8fe1d02e Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Thu, 22 May 2025 18:13:15 +0200 Subject: [PATCH 149/162] test: add smoke for csm extension --- .../contracts/test_csm_extension.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 tests/integration/contracts/test_csm_extension.py diff --git a/tests/integration/contracts/test_csm_extension.py b/tests/integration/contracts/test_csm_extension.py new file mode 100644 index 000000000..ea8fbdc9e --- /dev/null +++ b/tests/integration/contracts/test_csm_extension.py @@ -0,0 +1,23 @@ +from unittest.mock import Mock + +import pytest + +from src.web3py.extensions.csm import CSM +from src.web3py.types import Web3 + + +@pytest.fixture +def w3(web3_provider_integration): + web3_provider_integration.attach_modules({"csm": CSM}) + return web3_provider_integration + + +@pytest.mark.integration +@pytest.mark.skip("CSM v2 is not yet live") +def test_csm_extension(w3: Web3): + w3.csm.get_csm_last_processing_ref_slot(Mock(block_hash="latest")) + w3.csm.get_rewards_tree_root(Mock(block_hash="latest")) + w3.csm.get_rewards_tree_cid(Mock(block_hash="latest")) + w3.csm.get_curve_params(Mock(0), Mock(block_hash="latest")) + w3.csm.get_strikes_tree_root(Mock(block_hash="latest")) + w3.csm.get_strikes_tree_cid(Mock(block_hash="latest")) From 8911facff551f4e92e4972e7faf0d17fd5513780 Mon Sep 17 00:00:00 2001 From: chasingrainbows Date: Fri, 23 May 2025 17:18:19 +0300 Subject: [PATCH 150/162] feat(csm-v2): fix infinite recursion --- src/web3py/extensions/csm.py | 131 +++++++++++++++++++---------------- 1 file changed, 73 insertions(+), 58 deletions(-) diff --git a/src/web3py/extensions/csm.py b/src/web3py/extensions/csm.py index 347e5dc52..4262e2c03 100644 --- a/src/web3py/extensions/csm.py +++ b/src/web3py/extensions/csm.py @@ -33,6 +33,9 @@ class CSM(Module): module: CSModuleContract params: CSParametersRegistryContract + CONTRACT_LOAD_MAX_RETRIES: int = 100 + CONTRACT_LOAD_RETRY_DELAY: int = 60 + def __init__(self, w3: Web3) -> None: super().__init__(w3) self._load_contracts() @@ -69,64 +72,76 @@ def get_curve_params(self, no_id: NodeOperatorId, blockstamp: BlockStamp) -> Cur return CurveParams(perf_coeffs, perf_leeway_data, reward_share_data, strikes_params) def _load_contracts(self) -> None: - try: - self.module = cast( - CSModuleContract, - self.w3.eth.contract( - address=variables.CSM_MODULE_ADDRESS, # type: ignore - ContractFactoryClass=CSModuleContract, - decode_tuples=True, - ), - ) - - self.params = cast( - CSParametersRegistryContract, - self.w3.eth.contract( - address=self.module.parameters_registry(), - ContractFactoryClass=CSParametersRegistryContract, - decode_tuples=True, - ), - ) - - self.accounting = cast( - CSAccountingContract, - self.w3.eth.contract( - address=self.module.accounting(), - ContractFactoryClass=CSAccountingContract, - decode_tuples=True, - ), - ) - - self.fee_distributor = cast( - CSFeeDistributorContract, - self.w3.eth.contract( - address=self.accounting.fee_distributor(), - ContractFactoryClass=CSFeeDistributorContract, - decode_tuples=True, - ), - ) - - self.oracle = cast( - CSFeeOracleContract, - self.w3.eth.contract( - address=self.fee_distributor.oracle(), - ContractFactoryClass=CSFeeOracleContract, - decode_tuples=True, - ), - ) - - self.strikes = cast( - CSStrikesContract, - self.w3.eth.contract( - address=self.oracle.strikes(), - ContractFactoryClass=CSStrikesContract, - decode_tuples=True, - ), - ) - except Web3Exception as ex: - logger.error({"msg": "Some of the contracts aren't healthy", "error": str(ex)}) - sleep(60) - self._load_contracts() + last_error = None + + for attempt in range(self.CONTRACT_LOAD_MAX_RETRIES): + try: + self.module = cast( + CSModuleContract, + self.w3.eth.contract( + address=variables.CSM_MODULE_ADDRESS, # type: ignore + ContractFactoryClass=CSModuleContract, + decode_tuples=True, + ), + ) + + self.params = cast( + CSParametersRegistryContract, + self.w3.eth.contract( + address=self.module.parameters_registry(), + ContractFactoryClass=CSParametersRegistryContract, + decode_tuples=True, + ), + ) + + self.accounting = cast( + CSAccountingContract, + self.w3.eth.contract( + address=self.module.accounting(), + ContractFactoryClass=CSAccountingContract, + decode_tuples=True, + ), + ) + + self.fee_distributor = cast( + CSFeeDistributorContract, + self.w3.eth.contract( + address=self.accounting.fee_distributor(), + ContractFactoryClass=CSFeeDistributorContract, + decode_tuples=True, + ), + ) + + self.oracle = cast( + CSFeeOracleContract, + self.w3.eth.contract( + address=self.fee_distributor.oracle(), + ContractFactoryClass=CSFeeOracleContract, + decode_tuples=True, + ), + ) + + self.strikes = cast( + CSStrikesContract, + self.w3.eth.contract( + address=self.oracle.strikes(), + ContractFactoryClass=CSStrikesContract, + decode_tuples=True, + ), + ) + return + except Web3Exception as e: + last_error = e + logger.error({ + "msg": f"Attempt {attempt + 1}/{self.CONTRACT_LOAD_MAX_RETRIES} failed to load contracts", + "error": str(e) + }) + sleep(self.CONTRACT_LOAD_RETRY_DELAY) + + raise Web3Exception( + f"Failed to load contracts in CSM module " + f"after {self.CONTRACT_LOAD_MAX_RETRIES} attempts" + ) from last_error class LazyCSM(CSM): From 93ee2c51145505741eb47463293c8683ec2e26ab Mon Sep 17 00:00:00 2001 From: madlabman <10616301+madlabman@users.noreply.github.com> Date: Fri, 23 May 2025 17:36:00 +0200 Subject: [PATCH 151/162] chore: update CSM abis --- assets/CSAccounting.json | 2 +- assets/CSFeeDistributor.json | 2 +- assets/CSFeeOracle.json | 2 +- assets/CSModule.json | 2 +- assets/CSParametersRegistry.json | 2 +- assets/CSStrikes.json | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/assets/CSAccounting.json b/assets/CSAccounting.json index 9effc63b3..271d922d5 100644 --- a/assets/CSAccounting.json +++ b/assets/CSAccounting.json @@ -1 +1 @@ -[{"type":"constructor","inputs":[{"name":"lidoLocator","type":"address","internalType":"address"},{"name":"module","type":"address","internalType":"address"},{"name":"_feeDistributor","type":"address","internalType":"address"},{"name":"maxCurveLength","type":"uint256","internalType":"uint256"},{"name":"minBondLockPeriod","type":"uint256","internalType":"uint256"},{"name":"maxBondLockPeriod","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"DEFAULT_BOND_CURVE_ID","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"FEE_DISTRIBUTOR","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSFeeDistributor"}],"stateMutability":"view"},{"type":"function","name":"LIDO","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ILido"}],"stateMutability":"view"},{"type":"function","name":"LIDO_LOCATOR","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ILidoLocator"}],"stateMutability":"view"},{"type":"function","name":"MANAGE_BOND_CURVES_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"MAX_BOND_LOCK_PERIOD","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"MAX_CURVE_LENGTH","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"MIN_BOND_LOCK_PERIOD","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"MIN_CURVE_LENGTH","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"MODULE","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSModule"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_INFINITELY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RECOVERER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RESUME_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"SET_BOND_CURVE_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"WITHDRAWAL_QUEUE","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IWithdrawalQueue"}],"stateMutability":"view"},{"type":"function","name":"WSTETH","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IWstETH"}],"stateMutability":"view"},{"type":"function","name":"addBondCurve","inputs":[{"name":"bondCurve","type":"tuple[]","internalType":"struct ICSBondCurve.BondCurveIntervalInput[]","components":[{"name":"minKeysCount","type":"uint256","internalType":"uint256"},{"name":"trend","type":"uint256","internalType":"uint256"}]}],"outputs":[{"name":"id","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"chargeFee","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"chargePenaltyRecipient","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"claimRewardsStETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stETHAmount","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"claimedShares","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"claimRewardsUnstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stEthAmount","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"requestId","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"claimRewardsWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"wstETHAmount","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"claimedWstETH","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"compensateLockedBondETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"depositETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"depositETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"depositStETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stETHAmount","type":"uint256","internalType":"uint256"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"depositStETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stETHAmount","type":"uint256","internalType":"uint256"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"depositWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"wstETHAmount","type":"uint256","internalType":"uint256"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"depositWstETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"wstETHAmount","type":"uint256","internalType":"uint256"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"feeDistributor","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSFeeDistributor"}],"stateMutability":"view"},{"type":"function","name":"finalizeUpgradeV2","inputs":[{"name":"bondCurvesInputs","type":"tuple[][]","internalType":"struct ICSBondCurve.BondCurveIntervalInput[][]","components":[{"name":"minKeysCount","type":"uint256","internalType":"uint256"},{"name":"trend","type":"uint256","internalType":"uint256"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"getActualLockedBond","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBond","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondAmountByKeysCount","inputs":[{"name":"keys","type":"uint256","internalType":"uint256"},{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondAmountByKeysCountWstETH","inputs":[{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondCurve","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct ICSBondCurve.BondCurve","components":[{"name":"intervals","type":"tuple[]","internalType":"struct ICSBondCurve.BondCurveInterval[]","components":[{"name":"minKeysCount","type":"uint256","internalType":"uint256"},{"name":"minBond","type":"uint256","internalType":"uint256"},{"name":"trend","type":"uint256","internalType":"uint256"}]}]}],"stateMutability":"view"},{"type":"function","name":"getBondCurveId","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondLockPeriod","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondShares","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondSummary","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"current","type":"uint256","internalType":"uint256"},{"name":"required","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondSummaryShares","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"current","type":"uint256","internalType":"uint256"},{"name":"required","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getClaimableBondShares","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getClaimableRewardsAndBondShares","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"claimableShares","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getCurveInfo","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct ICSBondCurve.BondCurve","components":[{"name":"intervals","type":"tuple[]","internalType":"struct ICSBondCurve.BondCurveInterval[]","components":[{"name":"minKeysCount","type":"uint256","internalType":"uint256"},{"name":"minBond","type":"uint256","internalType":"uint256"},{"name":"trend","type":"uint256","internalType":"uint256"}]}]}],"stateMutability":"view"},{"type":"function","name":"getCurvesCount","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getInitializedVersion","inputs":[],"outputs":[{"name":"","type":"uint64","internalType":"uint64"}],"stateMutability":"view"},{"type":"function","name":"getKeysCountByBondAmount","inputs":[{"name":"amount","type":"uint256","internalType":"uint256"},{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getLockedBondInfo","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct ICSBondLock.BondLock","components":[{"name":"amount","type":"uint128","internalType":"uint128"},{"name":"until","type":"uint128","internalType":"uint128"}]}],"stateMutability":"view"},{"type":"function","name":"getRequiredBondForNextKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"additionalKeys","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRequiredBondForNextKeysWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"additionalKeys","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getResumeSinceTimestamp","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getUnbondedKeysCount","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getUnbondedKeysCountToEject","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"bondCurve","type":"tuple[]","internalType":"struct ICSBondCurve.BondCurveIntervalInput[]","components":[{"name":"minKeysCount","type":"uint256","internalType":"uint256"},{"name":"trend","type":"uint256","internalType":"uint256"}]},{"name":"admin","type":"address","internalType":"address"},{"name":"bondLockPeriod","type":"uint256","internalType":"uint256"},{"name":"_chargePenaltyRecipient","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"isPaused","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"lockBondETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"pauseFor","inputs":[{"name":"duration","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"penalize","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"pullFeeRewards","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC1155","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC20","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC721","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverEther","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverStETHShares","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"releaseLockedBondETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renewBurnerAllowance","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resume","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setBondCurve","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setBondLockPeriod","inputs":[{"name":"period","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setChargePenaltyRecipient","inputs":[{"name":"_chargePenaltyRecipient","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"settleLockedBondETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"applied","type":"bool","internalType":"bool"}],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"totalBondShares","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"updateBondCurve","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"bondCurve","type":"tuple[]","internalType":"struct ICSBondCurve.BondCurveIntervalInput[]","components":[{"name":"minKeysCount","type":"uint256","internalType":"uint256"},{"name":"trend","type":"uint256","internalType":"uint256"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"BondBurned","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"toBurnAmount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"burnedAmount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondCharged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"toChargeAmount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"chargedAmount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondClaimedStETH","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"to","type":"address","indexed":false,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondClaimedUnstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"to","type":"address","indexed":false,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"requestId","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondClaimedWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"to","type":"address","indexed":false,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondCurveAdded","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"bondCurveIntervals","type":"tuple[]","indexed":false,"internalType":"struct ICSBondCurve.BondCurveIntervalInput[]","components":[{"name":"minKeysCount","type":"uint256","internalType":"uint256"},{"name":"trend","type":"uint256","internalType":"uint256"}]}],"anonymous":false},{"type":"event","name":"BondCurveSet","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"curveId","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondCurveUpdated","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"bondCurveIntervals","type":"tuple[]","indexed":false,"internalType":"struct ICSBondCurve.BondCurveIntervalInput[]","components":[{"name":"minKeysCount","type":"uint256","internalType":"uint256"},{"name":"trend","type":"uint256","internalType":"uint256"}]}],"anonymous":false},{"type":"event","name":"BondDepositedETH","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"from","type":"address","indexed":false,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondDepositedStETH","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"from","type":"address","indexed":false,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondDepositedWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"from","type":"address","indexed":false,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondLockChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"newAmount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"until","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondLockCompensated","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondLockPeriodChanged","inputs":[{"name":"period","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondLockRemoved","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ChargePenaltyRecipientSet","inputs":[{"name":"chargePenaltyRecipient","type":"address","indexed":false,"internalType":"address"}],"anonymous":false},{"type":"event","name":"ERC1155Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC20Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC721Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"EtherRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"Paused","inputs":[{"name":"duration","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Resumed","inputs":[],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"StETHSharesRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"ElRewardsVaultReceiveFailed","inputs":[]},{"type":"error","name":"FailedToSendEther","inputs":[]},{"type":"error","name":"InvalidBondCurveId","inputs":[]},{"type":"error","name":"InvalidBondCurveLength","inputs":[]},{"type":"error","name":"InvalidBondCurveMaxLength","inputs":[]},{"type":"error","name":"InvalidBondCurveValues","inputs":[]},{"type":"error","name":"InvalidBondCurvesLength","inputs":[]},{"type":"error","name":"InvalidBondLockAmount","inputs":[]},{"type":"error","name":"InvalidBondLockPeriod","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidInitializationCurveId","inputs":[]},{"type":"error","name":"NodeOperatorDoesNotExist","inputs":[]},{"type":"error","name":"NotAllowedToRecover","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"NothingToClaim","inputs":[]},{"type":"error","name":"PauseUntilMustBeInFuture","inputs":[]},{"type":"error","name":"PausedExpected","inputs":[]},{"type":"error","name":"ResumedExpected","inputs":[]},{"type":"error","name":"SafeCastOverflowedUintDowncast","inputs":[{"name":"bits","type":"uint8","internalType":"uint8"},{"name":"value","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"SenderIsNotEligible","inputs":[]},{"type":"error","name":"SenderIsNotModule","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroChargePenaltyRecipientAddress","inputs":[]},{"type":"error","name":"ZeroFeeDistributorAddress","inputs":[]},{"type":"error","name":"ZeroLocatorAddress","inputs":[]},{"type":"error","name":"ZeroModuleAddress","inputs":[]},{"type":"error","name":"ZeroPauseDuration","inputs":[]}] \ No newline at end of file +[{"type":"constructor","inputs":[{"name":"lidoLocator","type":"address","internalType":"address"},{"name":"module","type":"address","internalType":"address"},{"name":"_feeDistributor","type":"address","internalType":"address"},{"name":"maxCurveLength","type":"uint256","internalType":"uint256"},{"name":"minBondLockPeriod","type":"uint256","internalType":"uint256"},{"name":"maxBondLockPeriod","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"DEFAULT_BOND_CURVE_ID","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"FEE_DISTRIBUTOR","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSFeeDistributor"}],"stateMutability":"view"},{"type":"function","name":"LIDO","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ILido"}],"stateMutability":"view"},{"type":"function","name":"LIDO_LOCATOR","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ILidoLocator"}],"stateMutability":"view"},{"type":"function","name":"MANAGE_BOND_CURVES_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"MAX_BOND_LOCK_PERIOD","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"MAX_CURVE_LENGTH","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"MIN_BOND_LOCK_PERIOD","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"MIN_CURVE_LENGTH","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"MODULE","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSModule"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_INFINITELY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RECOVERER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RESUME_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"SET_BOND_CURVE_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"WITHDRAWAL_QUEUE","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IWithdrawalQueue"}],"stateMutability":"view"},{"type":"function","name":"WSTETH","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IWstETH"}],"stateMutability":"view"},{"type":"function","name":"addBondCurve","inputs":[{"name":"bondCurve","type":"tuple[]","internalType":"struct ICSBondCurve.BondCurveIntervalInput[]","components":[{"name":"minKeysCount","type":"uint256","internalType":"uint256"},{"name":"trend","type":"uint256","internalType":"uint256"}]}],"outputs":[{"name":"id","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"chargeFee","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"chargePenaltyRecipient","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"claimRewardsStETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stETHAmount","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"claimedShares","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"claimRewardsUnstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stETHAmount","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"requestId","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"claimRewardsWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"wstETHAmount","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"claimedWstETH","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"compensateLockedBondETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"depositETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"depositETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"depositStETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stETHAmount","type":"uint256","internalType":"uint256"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"depositStETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"stETHAmount","type":"uint256","internalType":"uint256"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"depositWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"wstETHAmount","type":"uint256","internalType":"uint256"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"depositWstETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"wstETHAmount","type":"uint256","internalType":"uint256"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"feeDistributor","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSFeeDistributor"}],"stateMutability":"view"},{"type":"function","name":"finalizeUpgradeV2","inputs":[{"name":"bondCurvesInputs","type":"tuple[][]","internalType":"struct ICSBondCurve.BondCurveIntervalInput[][]","components":[{"name":"minKeysCount","type":"uint256","internalType":"uint256"},{"name":"trend","type":"uint256","internalType":"uint256"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"getActualLockedBond","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBond","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondAmountByKeysCount","inputs":[{"name":"keys","type":"uint256","internalType":"uint256"},{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondAmountByKeysCountWstETH","inputs":[{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondCurve","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct ICSBondCurve.BondCurve","components":[{"name":"intervals","type":"tuple[]","internalType":"struct ICSBondCurve.BondCurveInterval[]","components":[{"name":"minKeysCount","type":"uint256","internalType":"uint256"},{"name":"minBond","type":"uint256","internalType":"uint256"},{"name":"trend","type":"uint256","internalType":"uint256"}]}]}],"stateMutability":"view"},{"type":"function","name":"getBondCurveId","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondLockPeriod","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondShares","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondSummary","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"current","type":"uint256","internalType":"uint256"},{"name":"required","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBondSummaryShares","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"current","type":"uint256","internalType":"uint256"},{"name":"required","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getClaimableBondShares","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getClaimableRewardsAndBondShares","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"claimableShares","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getCurveInfo","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct ICSBondCurve.BondCurve","components":[{"name":"intervals","type":"tuple[]","internalType":"struct ICSBondCurve.BondCurveInterval[]","components":[{"name":"minKeysCount","type":"uint256","internalType":"uint256"},{"name":"minBond","type":"uint256","internalType":"uint256"},{"name":"trend","type":"uint256","internalType":"uint256"}]}]}],"stateMutability":"view"},{"type":"function","name":"getCurvesCount","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getInitializedVersion","inputs":[],"outputs":[{"name":"","type":"uint64","internalType":"uint64"}],"stateMutability":"view"},{"type":"function","name":"getKeysCountByBondAmount","inputs":[{"name":"amount","type":"uint256","internalType":"uint256"},{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getLockedBondInfo","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct ICSBondLock.BondLock","components":[{"name":"amount","type":"uint128","internalType":"uint128"},{"name":"until","type":"uint128","internalType":"uint128"}]}],"stateMutability":"view"},{"type":"function","name":"getRequiredBondForNextKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"additionalKeys","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRequiredBondForNextKeysWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"additionalKeys","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getResumeSinceTimestamp","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getUnbondedKeysCount","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getUnbondedKeysCountToEject","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"bondCurve","type":"tuple[]","internalType":"struct ICSBondCurve.BondCurveIntervalInput[]","components":[{"name":"minKeysCount","type":"uint256","internalType":"uint256"},{"name":"trend","type":"uint256","internalType":"uint256"}]},{"name":"admin","type":"address","internalType":"address"},{"name":"bondLockPeriod","type":"uint256","internalType":"uint256"},{"name":"_chargePenaltyRecipient","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"isPaused","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"lockBondETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"pauseFor","inputs":[{"name":"duration","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"penalize","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"pullFeeRewards","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"rewardsProof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC1155","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC20","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC721","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverEther","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverStETHShares","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"releaseLockedBondETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renewBurnerAllowance","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resume","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setBondCurve","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setBondLockPeriod","inputs":[{"name":"period","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setChargePenaltyRecipient","inputs":[{"name":"_chargePenaltyRecipient","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"settleLockedBondETH","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"applied","type":"bool","internalType":"bool"}],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"totalBondShares","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"updateBondCurve","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"bondCurve","type":"tuple[]","internalType":"struct ICSBondCurve.BondCurveIntervalInput[]","components":[{"name":"minKeysCount","type":"uint256","internalType":"uint256"},{"name":"trend","type":"uint256","internalType":"uint256"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"BondBurned","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"amountToBurn","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"burnedAmount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondCharged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"toChargeAmount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"chargedAmount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondClaimedStETH","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"to","type":"address","indexed":false,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondClaimedUnstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"to","type":"address","indexed":false,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"requestId","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondClaimedWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"to","type":"address","indexed":false,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondCurveAdded","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"bondCurveIntervals","type":"tuple[]","indexed":false,"internalType":"struct ICSBondCurve.BondCurveIntervalInput[]","components":[{"name":"minKeysCount","type":"uint256","internalType":"uint256"},{"name":"trend","type":"uint256","internalType":"uint256"}]}],"anonymous":false},{"type":"event","name":"BondCurveSet","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"curveId","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondCurveUpdated","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"bondCurveIntervals","type":"tuple[]","indexed":false,"internalType":"struct ICSBondCurve.BondCurveIntervalInput[]","components":[{"name":"minKeysCount","type":"uint256","internalType":"uint256"},{"name":"trend","type":"uint256","internalType":"uint256"}]}],"anonymous":false},{"type":"event","name":"BondDepositedETH","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"from","type":"address","indexed":false,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondDepositedStETH","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"from","type":"address","indexed":false,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondDepositedWstETH","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"from","type":"address","indexed":false,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondLockChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"newAmount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"until","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondLockCompensated","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondLockPeriodChanged","inputs":[{"name":"period","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BondLockRemoved","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ChargePenaltyRecipientSet","inputs":[{"name":"chargePenaltyRecipient","type":"address","indexed":false,"internalType":"address"}],"anonymous":false},{"type":"event","name":"ERC1155Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC20Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC721Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"EtherRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"Paused","inputs":[{"name":"duration","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Resumed","inputs":[],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"StETHSharesRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"ElRewardsVaultReceiveFailed","inputs":[]},{"type":"error","name":"FailedToSendEther","inputs":[]},{"type":"error","name":"InvalidBondCurveId","inputs":[]},{"type":"error","name":"InvalidBondCurveLength","inputs":[]},{"type":"error","name":"InvalidBondCurveMaxLength","inputs":[]},{"type":"error","name":"InvalidBondCurveValues","inputs":[]},{"type":"error","name":"InvalidBondCurvesLength","inputs":[]},{"type":"error","name":"InvalidBondLockAmount","inputs":[]},{"type":"error","name":"InvalidBondLockPeriod","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidInitializationCurveId","inputs":[]},{"type":"error","name":"NodeOperatorDoesNotExist","inputs":[]},{"type":"error","name":"NotAllowedToRecover","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"NothingToClaim","inputs":[]},{"type":"error","name":"PauseUntilMustBeInFuture","inputs":[]},{"type":"error","name":"PausedExpected","inputs":[]},{"type":"error","name":"ResumedExpected","inputs":[]},{"type":"error","name":"SafeCastOverflowedUintDowncast","inputs":[{"name":"bits","type":"uint8","internalType":"uint8"},{"name":"value","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"SenderIsNotEligible","inputs":[]},{"type":"error","name":"SenderIsNotModule","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroChargePenaltyRecipientAddress","inputs":[]},{"type":"error","name":"ZeroFeeDistributorAddress","inputs":[]},{"type":"error","name":"ZeroLocatorAddress","inputs":[]},{"type":"error","name":"ZeroModuleAddress","inputs":[]},{"type":"error","name":"ZeroPauseDuration","inputs":[]}] diff --git a/assets/CSFeeDistributor.json b/assets/CSFeeDistributor.json index df3d2142f..3682abbb8 100644 --- a/assets/CSFeeDistributor.json +++ b/assets/CSFeeDistributor.json @@ -1 +1 @@ -[{"type":"constructor","inputs":[{"name":"stETH","type":"address","internalType":"address"},{"name":"accounting","type":"address","internalType":"address"},{"name":"oracle","type":"address","internalType":"address"}],"stateMutability":"nonpayable"},{"type":"function","name":"ACCOUNTING","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"ORACLE","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"RECOVERER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"STETH","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IStETH"}],"stateMutability":"view"},{"type":"function","name":"distributeFees","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"shares","type":"uint256","internalType":"uint256"},{"name":"proof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"sharesToDistribute","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"distributedShares","inputs":[{"name":"","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"distributionDataHistoryCount","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"finalizeUpgradeV2","inputs":[{"name":"_rebateRecipient","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"getFeesToDistribute","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"shares","type":"uint256","internalType":"uint256"},{"name":"proof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"sharesToDistribute","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getHistoricalDistributionData","inputs":[{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct ICSFeeDistributor.DistributionData","components":[{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"treeRoot","type":"bytes32","internalType":"bytes32"},{"name":"treeCid","type":"string","internalType":"string"},{"name":"logCid","type":"string","internalType":"string"},{"name":"distributed","type":"uint256","internalType":"uint256"},{"name":"rebate","type":"uint256","internalType":"uint256"}]}],"stateMutability":"view"},{"type":"function","name":"getInitializedVersion","inputs":[],"outputs":[{"name":"","type":"uint64","internalType":"uint64"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"hashLeaf","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"shares","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"pure"},{"type":"function","name":"initialize","inputs":[{"name":"admin","type":"address","internalType":"address"},{"name":"_rebateRecipient","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"logCid","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"pendingSharesToDistribute","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"processOracleReport","inputs":[{"name":"_treeRoot","type":"bytes32","internalType":"bytes32"},{"name":"_treeCid","type":"string","internalType":"string"},{"name":"_logCid","type":"string","internalType":"string"},{"name":"distributed","type":"uint256","internalType":"uint256"},{"name":"rebate","type":"uint256","internalType":"uint256"},{"name":"refSlot","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"rebateRecipient","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"recoverERC1155","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC20","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC721","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverEther","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setRebateRecipient","inputs":[{"name":"_rebateRecipient","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"totalClaimableShares","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"treeCid","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"treeRoot","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"event","name":"DistributionDataUpdated","inputs":[{"name":"totalClaimableShares","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"treeRoot","type":"bytes32","indexed":false,"internalType":"bytes32"},{"name":"treeCid","type":"string","indexed":false,"internalType":"string"}],"anonymous":false},{"type":"event","name":"DistributionLogUpdated","inputs":[{"name":"logCid","type":"string","indexed":false,"internalType":"string"}],"anonymous":false},{"type":"event","name":"ERC1155Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC20Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC721Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"EtherRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"ModuleFeeDistributed","inputs":[{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"OperatorFeeDistributed","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"RebateRecipientSet","inputs":[{"name":"recipient","type":"address","indexed":false,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RebateTransferred","inputs":[{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"StETHSharesRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"FailedToSendEther","inputs":[]},{"type":"error","name":"FeeSharesDecrease","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidLogCID","inputs":[]},{"type":"error","name":"InvalidProof","inputs":[]},{"type":"error","name":"InvalidReportData","inputs":[]},{"type":"error","name":"InvalidShares","inputs":[]},{"type":"error","name":"InvalidTreeCid","inputs":[]},{"type":"error","name":"InvalidTreeRoot","inputs":[]},{"type":"error","name":"NotAccounting","inputs":[]},{"type":"error","name":"NotAllowedToRecover","inputs":[]},{"type":"error","name":"NotEnoughShares","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"NotOracle","inputs":[]},{"type":"error","name":"ZeroAccountingAddress","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroOracleAddress","inputs":[]},{"type":"error","name":"ZeroRebateRecipientAddress","inputs":[]},{"type":"error","name":"ZeroStEthAddress","inputs":[]}] \ No newline at end of file +[{"type":"constructor","inputs":[{"name":"stETH","type":"address","internalType":"address"},{"name":"accounting","type":"address","internalType":"address"},{"name":"oracle","type":"address","internalType":"address"}],"stateMutability":"nonpayable"},{"type":"function","name":"ACCOUNTING","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"ORACLE","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"RECOVERER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"STETH","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IStETH"}],"stateMutability":"view"},{"type":"function","name":"distributeFees","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"proof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"sharesToDistribute","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"distributedShares","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"distributed","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"distributionDataHistoryCount","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"finalizeUpgradeV2","inputs":[{"name":"_rebateRecipient","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"getFeesToDistribute","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"cumulativeFeeShares","type":"uint256","internalType":"uint256"},{"name":"proof","type":"bytes32[]","internalType":"bytes32[]"}],"outputs":[{"name":"sharesToDistribute","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getHistoricalDistributionData","inputs":[{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct ICSFeeDistributor.DistributionData","components":[{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"treeRoot","type":"bytes32","internalType":"bytes32"},{"name":"treeCid","type":"string","internalType":"string"},{"name":"logCid","type":"string","internalType":"string"},{"name":"distributed","type":"uint256","internalType":"uint256"},{"name":"rebate","type":"uint256","internalType":"uint256"}]}],"stateMutability":"view"},{"type":"function","name":"getInitializedVersion","inputs":[],"outputs":[{"name":"","type":"uint64","internalType":"uint64"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"hashLeaf","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"shares","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"pure"},{"type":"function","name":"initialize","inputs":[{"name":"admin","type":"address","internalType":"address"},{"name":"_rebateRecipient","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"logCid","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"pendingSharesToDistribute","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"processOracleReport","inputs":[{"name":"_treeRoot","type":"bytes32","internalType":"bytes32"},{"name":"_treeCid","type":"string","internalType":"string"},{"name":"_logCid","type":"string","internalType":"string"},{"name":"distributed","type":"uint256","internalType":"uint256"},{"name":"rebate","type":"uint256","internalType":"uint256"},{"name":"refSlot","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"rebateRecipient","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"recoverERC1155","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC20","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC721","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverEther","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setRebateRecipient","inputs":[{"name":"_rebateRecipient","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"totalClaimableShares","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"treeCid","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"treeRoot","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"event","name":"DistributionDataUpdated","inputs":[{"name":"totalClaimableShares","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"treeRoot","type":"bytes32","indexed":false,"internalType":"bytes32"},{"name":"treeCid","type":"string","indexed":false,"internalType":"string"}],"anonymous":false},{"type":"event","name":"DistributionLogUpdated","inputs":[{"name":"logCid","type":"string","indexed":false,"internalType":"string"}],"anonymous":false},{"type":"event","name":"ERC1155Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC20Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC721Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"EtherRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"ModuleFeeDistributed","inputs":[{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"OperatorFeeDistributed","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"RebateRecipientSet","inputs":[{"name":"recipient","type":"address","indexed":false,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RebateTransferred","inputs":[{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"StETHSharesRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"FailedToSendEther","inputs":[]},{"type":"error","name":"FeeSharesDecrease","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidLogCID","inputs":[]},{"type":"error","name":"InvalidProof","inputs":[]},{"type":"error","name":"InvalidReportData","inputs":[]},{"type":"error","name":"InvalidShares","inputs":[]},{"type":"error","name":"InvalidTreeCid","inputs":[]},{"type":"error","name":"InvalidTreeRoot","inputs":[]},{"type":"error","name":"NotAllowedToRecover","inputs":[]},{"type":"error","name":"NotEnoughShares","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"SenderIsNotAccounting","inputs":[]},{"type":"error","name":"SenderIsNotOracle","inputs":[]},{"type":"error","name":"ZeroAccountingAddress","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroOracleAddress","inputs":[]},{"type":"error","name":"ZeroRebateRecipientAddress","inputs":[]},{"type":"error","name":"ZeroStEthAddress","inputs":[]}] diff --git a/assets/CSFeeOracle.json b/assets/CSFeeOracle.json index f01871eb6..6d13c0c2c 100644 --- a/assets/CSFeeOracle.json +++ b/assets/CSFeeOracle.json @@ -1 +1 @@ -[{"type":"constructor","inputs":[{"name":"feeDistributor","type":"address","internalType":"address"},{"name":"strikes","type":"address","internalType":"address"},{"name":"secondsPerSlot","type":"uint256","internalType":"uint256"},{"name":"genesisTime","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"FEE_DISTRIBUTOR","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSFeeDistributor"}],"stateMutability":"view"},{"type":"function","name":"GENESIS_TIME","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"MANAGE_CONSENSUS_CONTRACT_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"MANAGE_CONSENSUS_VERSION_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_INFINITELY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RECOVERER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RESUME_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"SECONDS_PER_SLOT","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"STRIKES","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSStrikes"}],"stateMutability":"view"},{"type":"function","name":"SUBMIT_DATA_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"discardConsensusReport","inputs":[{"name":"refSlot","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"finalizeUpgradeV2","inputs":[{"name":"consensusVersion","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"getConsensusContract","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getConsensusReport","inputs":[],"outputs":[{"name":"hash","type":"bytes32","internalType":"bytes32"},{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"processingDeadlineTime","type":"uint256","internalType":"uint256"},{"name":"processingStarted","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"getConsensusVersion","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getContractVersion","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getLastProcessingRefSlot","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getResumeSinceTimestamp","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"admin","type":"address","internalType":"address"},{"name":"consensusContract","type":"address","internalType":"address"},{"name":"consensusVersion","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"isPaused","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"pauseFor","inputs":[{"name":"duration","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"pauseUntil","inputs":[{"name":"pauseUntilInclusive","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC1155","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC20","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC721","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverEther","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resume","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setConsensusContract","inputs":[{"name":"addr","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setConsensusVersion","inputs":[{"name":"version","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"submitConsensusReport","inputs":[{"name":"reportHash","type":"bytes32","internalType":"bytes32"},{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"submitReportData","inputs":[{"name":"data","type":"tuple","internalType":"struct ICSFeeOracle.ReportData","components":[{"name":"consensusVersion","type":"uint256","internalType":"uint256"},{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"treeRoot","type":"bytes32","internalType":"bytes32"},{"name":"treeCid","type":"string","internalType":"string"},{"name":"logCid","type":"string","internalType":"string"},{"name":"distributed","type":"uint256","internalType":"uint256"},{"name":"rebate","type":"uint256","internalType":"uint256"},{"name":"strikesTreeRoot","type":"bytes32","internalType":"bytes32"},{"name":"strikesTreeCid","type":"string","internalType":"string"}]},{"name":"contractVersion","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"event","name":"ConsensusHashContractSet","inputs":[{"name":"addr","type":"address","indexed":true,"internalType":"address"},{"name":"prevAddr","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"ConsensusVersionSet","inputs":[{"name":"version","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"prevVersion","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ContractVersionSet","inputs":[{"name":"version","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC1155Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC20Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC721Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"EtherRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"Paused","inputs":[{"name":"duration","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ProcessingStarted","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"hash","type":"bytes32","indexed":false,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"ReportDiscarded","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"hash","type":"bytes32","indexed":false,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"ReportSubmitted","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"hash","type":"bytes32","indexed":false,"internalType":"bytes32"},{"name":"processingDeadlineTime","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Resumed","inputs":[],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"StETHSharesRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"WarnProcessingMissed","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"AddressCannotBeSame","inputs":[]},{"type":"error","name":"AddressCannotBeZero","inputs":[]},{"type":"error","name":"FailedToSendEther","inputs":[]},{"type":"error","name":"HashCannotBeZero","inputs":[]},{"type":"error","name":"InitialRefSlotCannotBeLessThanProcessingOne","inputs":[{"name":"initialRefSlot","type":"uint256","internalType":"uint256"},{"name":"processingRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"InvalidContractVersion","inputs":[]},{"type":"error","name":"InvalidContractVersionIncrement","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"NoConsensusReportToProcess","inputs":[]},{"type":"error","name":"NonZeroContractVersionOnInit","inputs":[]},{"type":"error","name":"NotAllowedToRecover","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"PauseUntilMustBeInFuture","inputs":[]},{"type":"error","name":"PausedExpected","inputs":[]},{"type":"error","name":"ProcessingDeadlineMissed","inputs":[{"name":"deadline","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"RefSlotAlreadyProcessing","inputs":[]},{"type":"error","name":"RefSlotCannotDecrease","inputs":[{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"prevRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"RefSlotMustBeGreaterThanProcessingOne","inputs":[{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"processingRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"ResumedExpected","inputs":[]},{"type":"error","name":"SafeCastOverflowedUintDowncast","inputs":[{"name":"bits","type":"uint8","internalType":"uint8"},{"name":"value","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"SecondsPerSlotCannotBeZero","inputs":[]},{"type":"error","name":"SenderIsNotTheConsensusContract","inputs":[]},{"type":"error","name":"SenderNotAllowed","inputs":[]},{"type":"error","name":"UnexpectedChainConfig","inputs":[]},{"type":"error","name":"UnexpectedConsensusVersion","inputs":[{"name":"expectedVersion","type":"uint256","internalType":"uint256"},{"name":"receivedVersion","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"UnexpectedContractVersion","inputs":[{"name":"expected","type":"uint256","internalType":"uint256"},{"name":"received","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"UnexpectedDataHash","inputs":[{"name":"consensusHash","type":"bytes32","internalType":"bytes32"},{"name":"receivedHash","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"UnexpectedRefSlot","inputs":[{"name":"consensusRefSlot","type":"uint256","internalType":"uint256"},{"name":"dataRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"VersionCannotBeSame","inputs":[]},{"type":"error","name":"VersionCannotBeZero","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroFeeDistributorAddress","inputs":[]},{"type":"error","name":"ZeroPauseDuration","inputs":[]},{"type":"error","name":"ZeroStrikesAddress","inputs":[]}] \ No newline at end of file +[{"type":"constructor","inputs":[{"name":"feeDistributor","type":"address","internalType":"address"},{"name":"strikes","type":"address","internalType":"address"},{"name":"secondsPerSlot","type":"uint256","internalType":"uint256"},{"name":"genesisTime","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"FEE_DISTRIBUTOR","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSFeeDistributor"}],"stateMutability":"view"},{"type":"function","name":"GENESIS_TIME","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"MANAGE_CONSENSUS_CONTRACT_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"MANAGE_CONSENSUS_VERSION_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_INFINITELY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RECOVERER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RESUME_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"SECONDS_PER_SLOT","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"STRIKES","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSStrikes"}],"stateMutability":"view"},{"type":"function","name":"SUBMIT_DATA_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"discardConsensusReport","inputs":[{"name":"refSlot","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"finalizeUpgradeV2","inputs":[{"name":"consensusVersion","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"getConsensusContract","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getConsensusReport","inputs":[],"outputs":[{"name":"hash","type":"bytes32","internalType":"bytes32"},{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"processingDeadlineTime","type":"uint256","internalType":"uint256"},{"name":"processingStarted","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"getConsensusVersion","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getContractVersion","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getLastProcessingRefSlot","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getResumeSinceTimestamp","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"admin","type":"address","internalType":"address"},{"name":"consensusContract","type":"address","internalType":"address"},{"name":"consensusVersion","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"isPaused","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"pauseFor","inputs":[{"name":"duration","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"pauseUntil","inputs":[{"name":"pauseUntilInclusive","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC1155","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC20","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC721","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverEther","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resume","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setConsensusContract","inputs":[{"name":"addr","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setConsensusVersion","inputs":[{"name":"version","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"submitConsensusReport","inputs":[{"name":"reportHash","type":"bytes32","internalType":"bytes32"},{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"submitReportData","inputs":[{"name":"data","type":"tuple","internalType":"struct ICSFeeOracle.ReportData","components":[{"name":"consensusVersion","type":"uint256","internalType":"uint256"},{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"treeRoot","type":"bytes32","internalType":"bytes32"},{"name":"treeCid","type":"string","internalType":"string"},{"name":"logCid","type":"string","internalType":"string"},{"name":"distributed","type":"uint256","internalType":"uint256"},{"name":"rebate","type":"uint256","internalType":"uint256"},{"name":"strikesTreeRoot","type":"bytes32","internalType":"bytes32"},{"name":"strikesTreeCid","type":"string","internalType":"string"}]},{"name":"contractVersion","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"event","name":"ConsensusHashContractSet","inputs":[{"name":"addr","type":"address","indexed":true,"internalType":"address"},{"name":"prevAddr","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"ConsensusVersionSet","inputs":[{"name":"version","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"prevVersion","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ContractVersionSet","inputs":[{"name":"version","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC1155Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC20Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC721Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"EtherRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"Paused","inputs":[{"name":"duration","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ProcessingStarted","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"hash","type":"bytes32","indexed":false,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"ReportDiscarded","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"hash","type":"bytes32","indexed":false,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"ReportSubmitted","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"hash","type":"bytes32","indexed":false,"internalType":"bytes32"},{"name":"processingDeadlineTime","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Resumed","inputs":[],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"StETHSharesRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"WarnProcessingMissed","inputs":[{"name":"refSlot","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"AddressCannotBeSame","inputs":[]},{"type":"error","name":"AddressCannotBeZero","inputs":[]},{"type":"error","name":"FailedToSendEther","inputs":[]},{"type":"error","name":"HashCannotBeZero","inputs":[]},{"type":"error","name":"InitialRefSlotCannotBeLessThanProcessingOne","inputs":[{"name":"initialRefSlot","type":"uint256","internalType":"uint256"},{"name":"processingRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"InvalidContractVersion","inputs":[]},{"type":"error","name":"InvalidContractVersionIncrement","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"NoConsensusReportToProcess","inputs":[]},{"type":"error","name":"NonZeroContractVersionOnInit","inputs":[]},{"type":"error","name":"NotAllowedToRecover","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"PauseUntilMustBeInFuture","inputs":[]},{"type":"error","name":"PausedExpected","inputs":[]},{"type":"error","name":"ProcessingDeadlineMissed","inputs":[{"name":"deadline","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"RefSlotAlreadyProcessing","inputs":[]},{"type":"error","name":"RefSlotCannotDecrease","inputs":[{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"prevRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"RefSlotMustBeGreaterThanProcessingOne","inputs":[{"name":"refSlot","type":"uint256","internalType":"uint256"},{"name":"processingRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"ResumedExpected","inputs":[]},{"type":"error","name":"SafeCastOverflowedUintDowncast","inputs":[{"name":"bits","type":"uint8","internalType":"uint8"},{"name":"value","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"SecondsPerSlotCannotBeZero","inputs":[]},{"type":"error","name":"SenderIsNotTheConsensusContract","inputs":[]},{"type":"error","name":"SenderNotAllowed","inputs":[]},{"type":"error","name":"UnexpectedChainConfig","inputs":[]},{"type":"error","name":"UnexpectedConsensusVersion","inputs":[{"name":"expectedVersion","type":"uint256","internalType":"uint256"},{"name":"receivedVersion","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"UnexpectedContractVersion","inputs":[{"name":"expected","type":"uint256","internalType":"uint256"},{"name":"received","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"UnexpectedDataHash","inputs":[{"name":"consensusHash","type":"bytes32","internalType":"bytes32"},{"name":"receivedHash","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"UnexpectedRefSlot","inputs":[{"name":"consensusRefSlot","type":"uint256","internalType":"uint256"},{"name":"dataRefSlot","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"VersionCannotBeSame","inputs":[]},{"type":"error","name":"VersionCannotBeZero","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroFeeDistributorAddress","inputs":[]},{"type":"error","name":"ZeroPauseDuration","inputs":[]},{"type":"error","name":"ZeroStrikesAddress","inputs":[]}] diff --git a/assets/CSModule.json b/assets/CSModule.json index 22e10660e..4521e2966 100644 --- a/assets/CSModule.json +++ b/assets/CSModule.json @@ -1 +1 @@ -[{"type":"constructor","inputs":[{"name":"moduleType","type":"bytes32","internalType":"bytes32"},{"name":"lidoLocator","type":"address","internalType":"address"},{"name":"parametersRegistry","type":"address","internalType":"address"},{"name":"_accounting","type":"address","internalType":"address"},{"name":"exitPenalties","type":"address","internalType":"address"}],"stateMutability":"nonpayable"},{"type":"function","name":"ACCOUNTING","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSAccounting"}],"stateMutability":"view"},{"type":"function","name":"CREATE_NODE_OPERATOR_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"DEPOSIT_SIZE","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"EXIT_PENALTIES","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSExitPenalties"}],"stateMutability":"view"},{"type":"function","name":"LIDO_LOCATOR","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ILidoLocator"}],"stateMutability":"view"},{"type":"function","name":"PARAMETERS_REGISTRY","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSParametersRegistry"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_INFINITELY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"QUEUE_LEGACY_PRIORITY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"QUEUE_LOWEST_PRIORITY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"RECOVERER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"REPORT_EL_REWARDS_STEALING_PENALTY_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RESUME_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"SETTLE_EL_REWARDS_STEALING_PENALTY_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"STAKING_ROUTER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"STETH","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IStETH"}],"stateMutability":"view"},{"type":"function","name":"VERIFIER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"accounting","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSAccounting"}],"stateMutability":"view"},{"type":"function","name":"addValidatorKeysETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"addValidatorKeysStETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"addValidatorKeysWstETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"cancelELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"changeNodeOperatorRewardAddress","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"newAddress","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"cleanDepositQueue","inputs":[{"name":"maxItems","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"removed","type":"uint256","internalType":"uint256"},{"name":"lastRemovedAtDepth","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"compensateELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"confirmNodeOperatorManagerAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"confirmNodeOperatorRewardAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"createNodeOperator","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"managementProperties","type":"tuple","internalType":"struct NodeOperatorManagementProperties","components":[{"name":"managerAddress","type":"address","internalType":"address"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"extendedManagerPermissions","type":"bool","internalType":"bool"}]},{"name":"referrer","type":"address","internalType":"address"}],"outputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"decreaseVettedSigningKeysCount","inputs":[{"name":"nodeOperatorIds","type":"bytes","internalType":"bytes"},{"name":"vettedSigningKeysCounts","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"depositQueueItem","inputs":[{"name":"queuePriority","type":"uint256","internalType":"uint256"},{"name":"index","type":"uint128","internalType":"uint128"}],"outputs":[{"name":"","type":"uint256","internalType":"Batch"}],"stateMutability":"view"},{"type":"function","name":"depositQueuePointers","inputs":[{"name":"queuePriority","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"head","type":"uint128","internalType":"uint128"},{"name":"tail","type":"uint128","internalType":"uint128"}],"stateMutability":"view"},{"type":"function","name":"exitDeadlineThreshold","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"finalizeUpgradeV2","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"getActiveNodeOperatorsCount","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getInitializedVersion","inputs":[],"outputs":[{"name":"","type":"uint64","internalType":"uint64"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperator","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct NodeOperator","components":[{"name":"totalAddedKeys","type":"uint32","internalType":"uint32"},{"name":"totalWithdrawnKeys","type":"uint32","internalType":"uint32"},{"name":"totalDepositedKeys","type":"uint32","internalType":"uint32"},{"name":"totalVettedKeys","type":"uint32","internalType":"uint32"},{"name":"stuckValidatorsCount","type":"uint32","internalType":"uint32"},{"name":"depositableValidatorsCount","type":"uint32","internalType":"uint32"},{"name":"targetLimit","type":"uint32","internalType":"uint32"},{"name":"targetLimitMode","type":"uint8","internalType":"uint8"},{"name":"totalExitedKeys","type":"uint32","internalType":"uint32"},{"name":"enqueuedCount","type":"uint32","internalType":"uint32"},{"name":"managerAddress","type":"address","internalType":"address"},{"name":"proposedManagerAddress","type":"address","internalType":"address"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"proposedRewardAddress","type":"address","internalType":"address"},{"name":"extendedManagerPermissions","type":"bool","internalType":"bool"},{"name":"usedPriorityQueue","type":"bool","internalType":"bool"}]}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorIds","inputs":[{"name":"offset","type":"uint256","internalType":"uint256"},{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"nodeOperatorIds","type":"uint256[]","internalType":"uint256[]"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorIsActive","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorManagementProperties","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct NodeOperatorManagementProperties","components":[{"name":"managerAddress","type":"address","internalType":"address"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"extendedManagerPermissions","type":"bool","internalType":"bool"}]}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorNonWithdrawnKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorSummary","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"targetLimitMode","type":"uint256","internalType":"uint256"},{"name":"targetValidatorsCount","type":"uint256","internalType":"uint256"},{"name":"stuckValidatorsCount","type":"uint256","internalType":"uint256"},{"name":"refundedValidatorsCount","type":"uint256","internalType":"uint256"},{"name":"stuckPenaltyEndTimestamp","type":"uint256","internalType":"uint256"},{"name":"totalExitedValidators","type":"uint256","internalType":"uint256"},{"name":"totalDepositedValidators","type":"uint256","internalType":"uint256"},{"name":"depositableValidatorsCount","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorTotalDepositedKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"totalDepositedKeys","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorsCount","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNonce","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getResumeSinceTimestamp","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getSigningKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"startIndex","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bytes","internalType":"bytes"}],"stateMutability":"view"},{"type":"function","name":"getSigningKeysWithSignatures","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"startIndex","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"keys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"}],"stateMutability":"view"},{"type":"function","name":"getStakingModuleSummary","inputs":[],"outputs":[{"name":"totalExitedValidators","type":"uint256","internalType":"uint256"},{"name":"totalDepositedValidators","type":"uint256","internalType":"uint256"},{"name":"depositableValidatorsCount","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getType","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"admin","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"isPaused","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"isValidatorExitDelayPenaltyApplicable","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"","type":"uint256","internalType":"uint256"},{"name":"publicKey","type":"bytes","internalType":"bytes"},{"name":"eligibleToExitInSec","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"isValidatorWithdrawn","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"legacyQueue","inputs":[],"outputs":[{"name":"head","type":"uint128","internalType":"uint128"},{"name":"tail","type":"uint128","internalType":"uint128"}],"stateMutability":"view"},{"type":"function","name":"migrateToPriorityQueue","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"obtainDepositData","inputs":[{"name":"depositsCount","type":"uint256","internalType":"uint256"},{"name":"","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"}],"stateMutability":"nonpayable"},{"type":"function","name":"onExitedAndStuckValidatorsCountsUpdated","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"onRewardsMinted","inputs":[{"name":"totalShares","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"onValidatorExitTriggered","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"publicKey","type":"bytes","internalType":"bytes"},{"name":"withdrawalRequestPaidFee","type":"uint256","internalType":"uint256"},{"name":"exitType","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"onWithdrawalCredentialsChanged","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"pauseFor","inputs":[{"name":"duration","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"proposeNodeOperatorManagerAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"proposedAddress","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"proposeNodeOperatorRewardAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"proposedAddress","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC1155","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC20","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC721","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverEther","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"removeKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"startIndex","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"reportELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"blockHash","type":"bytes32","internalType":"bytes32"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"reportValidatorExitDelay","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"","type":"uint256","internalType":"uint256"},{"name":"publicKey","type":"bytes","internalType":"bytes"},{"name":"eligibleToExitInSec","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resetNodeOperatorManagerAddress","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resume","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"settleELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorIds","type":"uint256[]","internalType":"uint256[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"submitWithdrawals","inputs":[{"name":"withdrawalsInfo","type":"tuple[]","internalType":"struct ValidatorWithdrawalInfo[]","components":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"},{"name":"isSlashed","type":"bool","internalType":"bool"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"unsafeUpdateValidatorsCount","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"exitedValidatorsKeysCount","type":"uint256","internalType":"uint256"},{"name":"stuckValidatorsKeysCount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateDepositableValidatorsCount","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateExitedValidatorsCount","inputs":[{"name":"nodeOperatorIds","type":"bytes","internalType":"bytes"},{"name":"exitedValidatorsCounts","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateTargetValidatorsLimits","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"targetLimitMode","type":"uint256","internalType":"uint256"},{"name":"targetLimit","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"BatchEnqueued","inputs":[{"name":"queuePriority","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"count","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DelayedValidatorExitPenalized","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"penaltyValue","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DepositableSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"depositableKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DepositedSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"depositedKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltyCancelled","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltyCompensated","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltyReported","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"proposedBlockHash","type":"bytes32","indexed":false,"internalType":"bytes32"},{"name":"stolenAmount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltySettled","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC1155Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC20Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC721Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"EtherRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ExitedSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"exitedKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"KeyRemovalChargeApplied","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"NodeOperatorAdded","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"managerAddress","type":"address","indexed":true,"internalType":"address"},{"name":"rewardAddress","type":"address","indexed":true,"internalType":"address"},{"name":"extendedManagerPermissions","type":"bool","indexed":false,"internalType":"bool"}],"anonymous":false},{"type":"event","name":"NodeOperatorManagerAddressChangeProposed","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"oldProposedAddress","type":"address","indexed":true,"internalType":"address"},{"name":"newProposedAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NodeOperatorManagerAddressChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"oldAddress","type":"address","indexed":true,"internalType":"address"},{"name":"newAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NodeOperatorRewardAddressChangeProposed","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"oldProposedAddress","type":"address","indexed":true,"internalType":"address"},{"name":"newProposedAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NodeOperatorRewardAddressChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"oldAddress","type":"address","indexed":true,"internalType":"address"},{"name":"newAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NonceChanged","inputs":[{"name":"nonce","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Paused","inputs":[{"name":"duration","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ReferrerSet","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"referrer","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"Resumed","inputs":[],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"SigningKeyAdded","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"SigningKeyRemoved","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"StETHSharesRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"TargetValidatorsCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"targetLimitMode","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"targetValidatorsCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"TotalSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"totalKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"VettedSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"vettedKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"VettedSigningKeysCountDecreased","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"WithdrawalSubmitted","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"keyIndex","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"AlreadyProposed","inputs":[]},{"type":"error","name":"AlreadyWithdrawn","inputs":[]},{"type":"error","name":"CannotAddKeys","inputs":[]},{"type":"error","name":"EmptyKey","inputs":[]},{"type":"error","name":"ExitedKeysDecrease","inputs":[]},{"type":"error","name":"ExitedKeysHigherThanTotalDeposited","inputs":[]},{"type":"error","name":"FailedToSendEther","inputs":[]},{"type":"error","name":"InvalidAmount","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidInput","inputs":[]},{"type":"error","name":"InvalidKeysCount","inputs":[]},{"type":"error","name":"InvalidLength","inputs":[]},{"type":"error","name":"InvalidReportData","inputs":[]},{"type":"error","name":"InvalidVetKeysPointer","inputs":[]},{"type":"error","name":"KeysLimitExceeded","inputs":[]},{"type":"error","name":"MethodCallIsNotAllowed","inputs":[]},{"type":"error","name":"NodeOperatorDoesNotExist","inputs":[]},{"type":"error","name":"NotAllowedToRecover","inputs":[]},{"type":"error","name":"NotEnoughKeys","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"PauseUntilMustBeInFuture","inputs":[]},{"type":"error","name":"PausedExpected","inputs":[]},{"type":"error","name":"PriorityQueueAlreadyUsed","inputs":[]},{"type":"error","name":"QueueIsEmpty","inputs":[]},{"type":"error","name":"QueueLookupNoLimit","inputs":[]},{"type":"error","name":"ResumedExpected","inputs":[]},{"type":"error","name":"SameAddress","inputs":[]},{"type":"error","name":"SenderIsNotEligible","inputs":[]},{"type":"error","name":"SenderIsNotManagerAddress","inputs":[]},{"type":"error","name":"SenderIsNotProposedAddress","inputs":[]},{"type":"error","name":"SenderIsNotRewardAddress","inputs":[]},{"type":"error","name":"SigningKeysInvalidOffset","inputs":[]},{"type":"error","name":"ZeroAccountingAddress","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroExitPenaltiesAddress","inputs":[]},{"type":"error","name":"ZeroLocatorAddress","inputs":[]},{"type":"error","name":"ZeroParametersRegistryAddress","inputs":[]},{"type":"error","name":"ZeroPauseDuration","inputs":[]},{"type":"error","name":"ZeroRewardAddress","inputs":[]},{"type":"error","name":"ZeroSenderAddress","inputs":[]}] \ No newline at end of file +[{"type":"constructor","inputs":[{"name":"moduleType","type":"bytes32","internalType":"bytes32"},{"name":"lidoLocator","type":"address","internalType":"address"},{"name":"parametersRegistry","type":"address","internalType":"address"},{"name":"_accounting","type":"address","internalType":"address"},{"name":"exitPenalties","type":"address","internalType":"address"}],"stateMutability":"nonpayable"},{"type":"function","name":"ACCOUNTING","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSAccounting"}],"stateMutability":"view"},{"type":"function","name":"CREATE_NODE_OPERATOR_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"DEPOSIT_SIZE","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"EXIT_PENALTIES","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSExitPenalties"}],"stateMutability":"view"},{"type":"function","name":"FEE_DISTRIBUTOR","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"LIDO_LOCATOR","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ILidoLocator"}],"stateMutability":"view"},{"type":"function","name":"PARAMETERS_REGISTRY","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSParametersRegistry"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_INFINITELY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"PAUSE_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"QUEUE_LEGACY_PRIORITY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"QUEUE_LOWEST_PRIORITY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"RECOVERER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"REPORT_EL_REWARDS_STEALING_PENALTY_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"RESUME_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"SETTLE_EL_REWARDS_STEALING_PENALTY_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"STAKING_ROUTER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"STETH","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract IStETH"}],"stateMutability":"view"},{"type":"function","name":"VERIFIER_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"accounting","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSAccounting"}],"stateMutability":"view"},{"type":"function","name":"addValidatorKeysETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"addValidatorKeysStETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"addValidatorKeysWstETH","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"},{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"},{"name":"permit","type":"tuple","internalType":"struct ICSAccounting.PermitInput","components":[{"name":"value","type":"uint256","internalType":"uint256"},{"name":"deadline","type":"uint256","internalType":"uint256"},{"name":"v","type":"uint8","internalType":"uint8"},{"name":"r","type":"bytes32","internalType":"bytes32"},{"name":"s","type":"bytes32","internalType":"bytes32"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"cancelELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"changeNodeOperatorRewardAddress","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"newAddress","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"cleanDepositQueue","inputs":[{"name":"maxItems","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"removed","type":"uint256","internalType":"uint256"},{"name":"lastRemovedAtDepth","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"compensateELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"confirmNodeOperatorManagerAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"confirmNodeOperatorRewardAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"createNodeOperator","inputs":[{"name":"from","type":"address","internalType":"address"},{"name":"managementProperties","type":"tuple","internalType":"struct NodeOperatorManagementProperties","components":[{"name":"managerAddress","type":"address","internalType":"address"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"extendedManagerPermissions","type":"bool","internalType":"bool"}]},{"name":"referrer","type":"address","internalType":"address"}],"outputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"decreaseVettedSigningKeysCount","inputs":[{"name":"nodeOperatorIds","type":"bytes","internalType":"bytes"},{"name":"vettedSigningKeysCounts","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"depositQueueItem","inputs":[{"name":"queuePriority","type":"uint256","internalType":"uint256"},{"name":"index","type":"uint128","internalType":"uint128"}],"outputs":[{"name":"","type":"uint256","internalType":"Batch"}],"stateMutability":"view"},{"type":"function","name":"depositQueuePointers","inputs":[{"name":"queuePriority","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"head","type":"uint128","internalType":"uint128"},{"name":"tail","type":"uint128","internalType":"uint128"}],"stateMutability":"view"},{"type":"function","name":"exitDeadlineThreshold","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"finalizeUpgradeV2","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"getActiveNodeOperatorsCount","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getInitializedVersion","inputs":[],"outputs":[{"name":"","type":"uint64","internalType":"uint64"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperator","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct NodeOperator","components":[{"name":"totalAddedKeys","type":"uint32","internalType":"uint32"},{"name":"totalWithdrawnKeys","type":"uint32","internalType":"uint32"},{"name":"totalDepositedKeys","type":"uint32","internalType":"uint32"},{"name":"totalVettedKeys","type":"uint32","internalType":"uint32"},{"name":"stuckValidatorsCount","type":"uint32","internalType":"uint32"},{"name":"depositableValidatorsCount","type":"uint32","internalType":"uint32"},{"name":"targetLimit","type":"uint32","internalType":"uint32"},{"name":"targetLimitMode","type":"uint8","internalType":"uint8"},{"name":"totalExitedKeys","type":"uint32","internalType":"uint32"},{"name":"enqueuedCount","type":"uint32","internalType":"uint32"},{"name":"managerAddress","type":"address","internalType":"address"},{"name":"proposedManagerAddress","type":"address","internalType":"address"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"proposedRewardAddress","type":"address","internalType":"address"},{"name":"extendedManagerPermissions","type":"bool","internalType":"bool"},{"name":"usedPriorityQueue","type":"bool","internalType":"bool"}]}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorIds","inputs":[{"name":"offset","type":"uint256","internalType":"uint256"},{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"nodeOperatorIds","type":"uint256[]","internalType":"uint256[]"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorIsActive","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorManagementProperties","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"tuple","internalType":"struct NodeOperatorManagementProperties","components":[{"name":"managerAddress","type":"address","internalType":"address"},{"name":"rewardAddress","type":"address","internalType":"address"},{"name":"extendedManagerPermissions","type":"bool","internalType":"bool"}]}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorNonWithdrawnKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorOwner","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorSummary","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"targetLimitMode","type":"uint256","internalType":"uint256"},{"name":"targetValidatorsCount","type":"uint256","internalType":"uint256"},{"name":"stuckValidatorsCount","type":"uint256","internalType":"uint256"},{"name":"refundedValidatorsCount","type":"uint256","internalType":"uint256"},{"name":"stuckPenaltyEndTimestamp","type":"uint256","internalType":"uint256"},{"name":"totalExitedValidators","type":"uint256","internalType":"uint256"},{"name":"totalDepositedValidators","type":"uint256","internalType":"uint256"},{"name":"depositableValidatorsCount","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorTotalDepositedKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"totalDepositedKeys","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNodeOperatorsCount","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getNonce","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getResumeSinceTimestamp","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getSigningKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"startIndex","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bytes","internalType":"bytes"}],"stateMutability":"view"},{"type":"function","name":"getSigningKeysWithSignatures","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"startIndex","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"keys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"}],"stateMutability":"view"},{"type":"function","name":"getStakingModuleSummary","inputs":[],"outputs":[{"name":"totalExitedValidators","type":"uint256","internalType":"uint256"},{"name":"totalDepositedValidators","type":"uint256","internalType":"uint256"},{"name":"depositableValidatorsCount","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getType","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"admin","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"isPaused","inputs":[],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"isValidatorExitDelayPenaltyApplicable","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"","type":"uint256","internalType":"uint256"},{"name":"publicKey","type":"bytes","internalType":"bytes"},{"name":"eligibleToExitInSec","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"isValidatorWithdrawn","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"legacyQueue","inputs":[],"outputs":[{"name":"head","type":"uint128","internalType":"uint128"},{"name":"tail","type":"uint128","internalType":"uint128"}],"stateMutability":"view"},{"type":"function","name":"migrateToPriorityQueue","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"obtainDepositData","inputs":[{"name":"depositsCount","type":"uint256","internalType":"uint256"},{"name":"","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"publicKeys","type":"bytes","internalType":"bytes"},{"name":"signatures","type":"bytes","internalType":"bytes"}],"stateMutability":"nonpayable"},{"type":"function","name":"onExitedAndStuckValidatorsCountsUpdated","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"onRewardsMinted","inputs":[{"name":"totalShares","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"onValidatorExitTriggered","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"publicKey","type":"bytes","internalType":"bytes"},{"name":"withdrawalRequestPaidFee","type":"uint256","internalType":"uint256"},{"name":"exitType","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"onWithdrawalCredentialsChanged","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"pauseFor","inputs":[{"name":"duration","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"proposeNodeOperatorManagerAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"proposedAddress","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"proposeNodeOperatorRewardAddressChange","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"proposedAddress","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC1155","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC20","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverERC721","inputs":[{"name":"token","type":"address","internalType":"address"},{"name":"tokenId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"recoverEther","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"removeKeys","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"startIndex","type":"uint256","internalType":"uint256"},{"name":"keysCount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"reportELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"blockHash","type":"bytes32","internalType":"bytes32"},{"name":"amount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"reportValidatorExitDelay","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"","type":"uint256","internalType":"uint256"},{"name":"publicKey","type":"bytes","internalType":"bytes"},{"name":"eligibleToExitInSec","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resetNodeOperatorManagerAddress","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"resume","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"settleELRewardsStealingPenalty","inputs":[{"name":"nodeOperatorIds","type":"uint256[]","internalType":"uint256[]"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"submitWithdrawals","inputs":[{"name":"withdrawalsInfo","type":"tuple[]","internalType":"struct ValidatorWithdrawalInfo[]","components":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"},{"name":"amount","type":"uint256","internalType":"uint256"},{"name":"isSlashed","type":"bool","internalType":"bool"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"unsafeUpdateValidatorsCount","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"exitedValidatorsKeysCount","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateDepositableValidatorsCount","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateExitedValidatorsCount","inputs":[{"name":"nodeOperatorIds","type":"bytes","internalType":"bytes"},{"name":"exitedValidatorsCounts","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"updateTargetValidatorsLimits","inputs":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"targetLimitMode","type":"uint256","internalType":"uint256"},{"name":"targetLimit","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"BatchEnqueued","inputs":[{"name":"queuePriority","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"count","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DelayedValidatorExitPenalized","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"penaltyValue","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DepositableSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"depositableKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DepositedSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"depositedKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltyCancelled","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltyCompensated","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltyReported","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"proposedBlockHash","type":"bytes32","indexed":false,"internalType":"bytes32"},{"name":"stolenAmount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ELRewardsStealingPenaltySettled","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC1155Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC20Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ERC721Recovered","inputs":[{"name":"token","type":"address","indexed":true,"internalType":"address"},{"name":"tokenId","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"recipient","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"EtherRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ExitedSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"exitedKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"KeyRemovalChargeApplied","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"NodeOperatorAdded","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"managerAddress","type":"address","indexed":true,"internalType":"address"},{"name":"rewardAddress","type":"address","indexed":true,"internalType":"address"},{"name":"extendedManagerPermissions","type":"bool","indexed":false,"internalType":"bool"}],"anonymous":false},{"type":"event","name":"NodeOperatorManagerAddressChangeProposed","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"oldProposedAddress","type":"address","indexed":true,"internalType":"address"},{"name":"newProposedAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NodeOperatorManagerAddressChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"oldAddress","type":"address","indexed":true,"internalType":"address"},{"name":"newAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NodeOperatorRewardAddressChangeProposed","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"oldProposedAddress","type":"address","indexed":true,"internalType":"address"},{"name":"newProposedAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NodeOperatorRewardAddressChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"oldAddress","type":"address","indexed":true,"internalType":"address"},{"name":"newAddress","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"NonceChanged","inputs":[{"name":"nonce","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Paused","inputs":[{"name":"duration","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ReferrerSet","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"referrer","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"Resumed","inputs":[],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"SigningKeyAdded","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"SigningKeyRemoved","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"event","name":"StETHSharesRecovered","inputs":[{"name":"recipient","type":"address","indexed":true,"internalType":"address"},{"name":"shares","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"TargetValidatorsCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"targetLimitMode","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"targetValidatorsCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"TotalSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"totalKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"VettedSigningKeysCountChanged","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"vettedKeysCount","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"VettedSigningKeysCountDecreased","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"WithdrawalSubmitted","inputs":[{"name":"nodeOperatorId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"keyIndex","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"amount","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"pubkey","type":"bytes","indexed":false,"internalType":"bytes"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"AlreadyProposed","inputs":[]},{"type":"error","name":"AlreadyWithdrawn","inputs":[]},{"type":"error","name":"CannotAddKeys","inputs":[]},{"type":"error","name":"EmptyKey","inputs":[]},{"type":"error","name":"ExitedKeysDecrease","inputs":[]},{"type":"error","name":"ExitedKeysHigherThanTotalDeposited","inputs":[]},{"type":"error","name":"FailedToSendEther","inputs":[]},{"type":"error","name":"InvalidAmount","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidInput","inputs":[]},{"type":"error","name":"InvalidKeysCount","inputs":[]},{"type":"error","name":"InvalidLength","inputs":[]},{"type":"error","name":"InvalidReportData","inputs":[]},{"type":"error","name":"InvalidVetKeysPointer","inputs":[]},{"type":"error","name":"KeysLimitExceeded","inputs":[]},{"type":"error","name":"MethodCallIsNotAllowed","inputs":[]},{"type":"error","name":"NodeOperatorDoesNotExist","inputs":[]},{"type":"error","name":"NotAllowedToRecover","inputs":[]},{"type":"error","name":"NotEnoughKeys","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"PauseUntilMustBeInFuture","inputs":[]},{"type":"error","name":"PausedExpected","inputs":[]},{"type":"error","name":"PriorityQueueAlreadyUsed","inputs":[]},{"type":"error","name":"QueueIsEmpty","inputs":[]},{"type":"error","name":"QueueLookupNoLimit","inputs":[]},{"type":"error","name":"ResumedExpected","inputs":[]},{"type":"error","name":"SameAddress","inputs":[]},{"type":"error","name":"SenderIsNotEligible","inputs":[]},{"type":"error","name":"SenderIsNotManagerAddress","inputs":[]},{"type":"error","name":"SenderIsNotProposedAddress","inputs":[]},{"type":"error","name":"SenderIsNotRewardAddress","inputs":[]},{"type":"error","name":"SigningKeysInvalidOffset","inputs":[]},{"type":"error","name":"ZeroAccountingAddress","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroExitPenaltiesAddress","inputs":[]},{"type":"error","name":"ZeroLocatorAddress","inputs":[]},{"type":"error","name":"ZeroParametersRegistryAddress","inputs":[]},{"type":"error","name":"ZeroPauseDuration","inputs":[]},{"type":"error","name":"ZeroRewardAddress","inputs":[]},{"type":"error","name":"ZeroSenderAddress","inputs":[]}] diff --git a/assets/CSParametersRegistry.json b/assets/CSParametersRegistry.json index 325cb76ce..43f093fac 100644 --- a/assets/CSParametersRegistry.json +++ b/assets/CSParametersRegistry.json @@ -1 +1 @@ -[{"type":"constructor","inputs":[{"name":"queueLowestPriority","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"QUEUE_LEGACY_PRIORITY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"QUEUE_LOWEST_PRIORITY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultAllowedExitDelay","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultBadPerformancePenalty","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultElRewardsStealingAdditionalFine","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultExitDelayPenalty","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultKeyRemovalCharge","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultKeysLimit","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultMaxWithdrawalRequestFee","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultPerformanceCoefficients","inputs":[],"outputs":[{"name":"attestationsWeight","type":"uint32","internalType":"uint32"},{"name":"blocksWeight","type":"uint32","internalType":"uint32"},{"name":"syncWeight","type":"uint32","internalType":"uint32"}],"stateMutability":"view"},{"type":"function","name":"defaultPerformanceLeeway","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultQueueConfig","inputs":[],"outputs":[{"name":"priority","type":"uint32","internalType":"uint32"},{"name":"maxDeposits","type":"uint32","internalType":"uint32"}],"stateMutability":"view"},{"type":"function","name":"defaultRewardShare","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultStrikesParams","inputs":[],"outputs":[{"name":"lifetime","type":"uint32","internalType":"uint32"},{"name":"threshold","type":"uint32","internalType":"uint32"}],"stateMutability":"view"},{"type":"function","name":"getAllowedExitDelay","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"delay","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBadPerformancePenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"penalty","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getElRewardsStealingAdditionalFine","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"fine","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getExitDelayPenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"penalty","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getInitializedVersion","inputs":[],"outputs":[{"name":"","type":"uint64","internalType":"uint64"}],"stateMutability":"view"},{"type":"function","name":"getKeyRemovalCharge","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"keyRemovalCharge","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getKeysLimit","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"limit","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getMaxWithdrawalRequestFee","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"fee","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getPerformanceCoefficients","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"attestationsWeight","type":"uint256","internalType":"uint256"},{"name":"blocksWeight","type":"uint256","internalType":"uint256"},{"name":"syncWeight","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getPerformanceLeewayData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"data","type":"tuple","internalType":"struct ICSParametersRegistry.KeyNumberValue","components":[{"name":"intervals","type":"tuple[]","internalType":"struct ICSParametersRegistry.KeyNumberValueInterval[]","components":[{"name":"minKeyNumber","type":"uint256","internalType":"uint256"},{"name":"value","type":"uint256","internalType":"uint256"}]}]}],"stateMutability":"view"},{"type":"function","name":"getQueueConfig","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"queuePriority","type":"uint32","internalType":"uint32"},{"name":"maxDeposits","type":"uint32","internalType":"uint32"}],"stateMutability":"view"},{"type":"function","name":"getRewardShareData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"data","type":"tuple","internalType":"struct ICSParametersRegistry.KeyNumberValue","components":[{"name":"intervals","type":"tuple[]","internalType":"struct ICSParametersRegistry.KeyNumberValueInterval[]","components":[{"name":"minKeyNumber","type":"uint256","internalType":"uint256"},{"name":"value","type":"uint256","internalType":"uint256"}]}]}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getStrikesParams","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"lifetime","type":"uint256","internalType":"uint256"},{"name":"threshold","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"admin","type":"address","internalType":"address"},{"name":"data","type":"tuple","internalType":"struct ICSParametersRegistry.InitializationData","components":[{"name":"keyRemovalCharge","type":"uint256","internalType":"uint256"},{"name":"elRewardsStealingAdditionalFine","type":"uint256","internalType":"uint256"},{"name":"keysLimit","type":"uint256","internalType":"uint256"},{"name":"rewardShare","type":"uint256","internalType":"uint256"},{"name":"performanceLeeway","type":"uint256","internalType":"uint256"},{"name":"strikesLifetime","type":"uint256","internalType":"uint256"},{"name":"strikesThreshold","type":"uint256","internalType":"uint256"},{"name":"defaultQueuePriority","type":"uint256","internalType":"uint256"},{"name":"defaultQueueMaxDeposits","type":"uint256","internalType":"uint256"},{"name":"badPerformancePenalty","type":"uint256","internalType":"uint256"},{"name":"attestationsWeight","type":"uint256","internalType":"uint256"},{"name":"blocksWeight","type":"uint256","internalType":"uint256"},{"name":"syncWeight","type":"uint256","internalType":"uint256"},{"name":"defaultAllowedExitDelay","type":"uint256","internalType":"uint256"},{"name":"defaultExitDelayPenalty","type":"uint256","internalType":"uint256"},{"name":"defaultMaxWithdrawalRequestFee","type":"uint256","internalType":"uint256"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setAllowedExitDelay","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"delay","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setBadPerformancePenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"penalty","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultAllowedExitDelay","inputs":[{"name":"delay","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultBadPerformancePenalty","inputs":[{"name":"penalty","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultElRewardsStealingAdditionalFine","inputs":[{"name":"fine","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultExitDelayPenalty","inputs":[{"name":"penalty","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultKeyRemovalCharge","inputs":[{"name":"keyRemovalCharge","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultKeysLimit","inputs":[{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultMaxWithdrawalRequestFee","inputs":[{"name":"fee","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultPerformanceCoefficients","inputs":[{"name":"attestationsWeight","type":"uint256","internalType":"uint256"},{"name":"blocksWeight","type":"uint256","internalType":"uint256"},{"name":"syncWeight","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultPerformanceLeeway","inputs":[{"name":"leeway","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultQueueConfig","inputs":[{"name":"priority","type":"uint256","internalType":"uint256"},{"name":"maxDeposits","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultRewardShare","inputs":[{"name":"share","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultStrikesParams","inputs":[{"name":"lifetime","type":"uint256","internalType":"uint256"},{"name":"threshold","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setElRewardsStealingAdditionalFine","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"fine","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setExitDelayPenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"penalty","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setKeyRemovalCharge","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"keyRemovalCharge","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setKeysLimit","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setMaxWithdrawalRequestFee","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"fee","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setPerformanceCoefficients","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"attestationsWeight","type":"uint256","internalType":"uint256"},{"name":"blocksWeight","type":"uint256","internalType":"uint256"},{"name":"syncWeight","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setPerformanceLeewayData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"data","type":"tuple[]","internalType":"struct ICSParametersRegistry.KeyNumberValueInterval[]","components":[{"name":"minKeyNumber","type":"uint256","internalType":"uint256"},{"name":"value","type":"uint256","internalType":"uint256"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setQueueConfig","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"priority","type":"uint32","internalType":"uint32"},{"name":"maxDeposits","type":"uint32","internalType":"uint32"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setRewardShareData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"data","type":"tuple[]","internalType":"struct ICSParametersRegistry.KeyNumberValueInterval[]","components":[{"name":"minKeyNumber","type":"uint256","internalType":"uint256"},{"name":"value","type":"uint256","internalType":"uint256"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setStrikesParams","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"lifetime","type":"uint256","internalType":"uint256"},{"name":"threshold","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"unsetAllowedExitDelay","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetBadPerformancePenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetElRewardsStealingAdditionalFine","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetExitDelayPenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetKeyRemovalCharge","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetKeysLimit","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetMaxWithdrawalRequestFee","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetPerformanceCoefficients","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetPerformanceLeewayData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetQueueConfig","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetRewardShareData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetStrikesParams","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"AllowedExitDelaySet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"delay","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"AllowedExitDelayUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BadPerformancePenaltySet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"penalty","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BadPerformancePenaltyUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultAllowedExitDelaySet","inputs":[{"name":"delay","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultBadPerformancePenaltySet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultElRewardsStealingAdditionalFineSet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultExitDelayPenaltySet","inputs":[{"name":"penalty","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultKeyRemovalChargeSet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultKeysLimitSet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultMaxWithdrawalRequestFeeSet","inputs":[{"name":"fee","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultPerformanceCoefficientsSet","inputs":[{"name":"attestationsWeight","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"blocksWeight","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"syncWeight","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultPerformanceLeewaySet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultQueueConfigSet","inputs":[{"name":"priority","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"maxDeposits","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultRewardShareSet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultStrikesParamsSet","inputs":[{"name":"lifetime","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"threshold","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ElRewardsStealingAdditionalFineSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"fine","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ElRewardsStealingAdditionalFineUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ExitDelayPenaltySet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"penalty","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ExitDelayPenaltyUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"KeyRemovalChargeSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"keyRemovalCharge","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"KeyRemovalChargeUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"KeysLimitSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"limit","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"KeysLimitUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"MaxWithdrawalRequestFeeSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"fee","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"MaxWithdrawalRequestFeeUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PerformanceCoefficientsSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"attestationsWeight","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"blocksWeight","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"syncWeight","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PerformanceCoefficientsUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PerformanceLeewayDataSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"data","type":"tuple[]","indexed":false,"internalType":"struct ICSParametersRegistry.KeyNumberValueInterval[]","components":[{"name":"minKeyNumber","type":"uint256","internalType":"uint256"},{"name":"value","type":"uint256","internalType":"uint256"}]}],"anonymous":false},{"type":"event","name":"PerformanceLeewayDataUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"QueueConfigSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"priority","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"maxDeposits","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"QueueConfigUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"RewardShareDataSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"data","type":"tuple[]","indexed":false,"internalType":"struct ICSParametersRegistry.KeyNumberValueInterval[]","components":[{"name":"minKeyNumber","type":"uint256","internalType":"uint256"},{"name":"value","type":"uint256","internalType":"uint256"}]}],"anonymous":false},{"type":"event","name":"RewardShareDataUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"StrikesParamsSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"lifetime","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"threshold","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"StrikesParamsUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"InvalidExitDelayPenalty","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidKeyIndexValueIntervals","inputs":[]},{"type":"error","name":"InvalidPerformanceCoefficients","inputs":[]},{"type":"error","name":"InvalidPerformanceLeewayData","inputs":[]},{"type":"error","name":"InvalidRewardShareData","inputs":[]},{"type":"error","name":"InvalidStrikesParams","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"QueueCannotBeUsed","inputs":[]},{"type":"error","name":"SafeCastOverflowedUintDowncast","inputs":[{"name":"bits","type":"uint8","internalType":"uint8"},{"name":"value","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroMaxDeposits","inputs":[]}] \ No newline at end of file +[{"type":"constructor","inputs":[{"name":"queueLowestPriority","type":"uint256","internalType":"uint256"}],"stateMutability":"nonpayable"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"QUEUE_LEGACY_PRIORITY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"QUEUE_LOWEST_PRIORITY","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultAllowedExitDelay","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultBadPerformancePenalty","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultElRewardsStealingAdditionalFine","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultExitDelayPenalty","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultKeyRemovalCharge","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultKeysLimit","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultMaxWithdrawalRequestFee","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultPerformanceCoefficients","inputs":[],"outputs":[{"name":"attestationsWeight","type":"uint32","internalType":"uint32"},{"name":"blocksWeight","type":"uint32","internalType":"uint32"},{"name":"syncWeight","type":"uint32","internalType":"uint32"}],"stateMutability":"view"},{"type":"function","name":"defaultPerformanceLeeway","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultQueueConfig","inputs":[],"outputs":[{"name":"priority","type":"uint32","internalType":"uint32"},{"name":"maxDeposits","type":"uint32","internalType":"uint32"}],"stateMutability":"view"},{"type":"function","name":"defaultRewardShare","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"defaultStrikesParams","inputs":[],"outputs":[{"name":"lifetime","type":"uint32","internalType":"uint32"},{"name":"threshold","type":"uint32","internalType":"uint32"}],"stateMutability":"view"},{"type":"function","name":"getAllowedExitDelay","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"delay","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getBadPerformancePenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"penalty","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getElRewardsStealingAdditionalFine","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"fine","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getExitDelayPenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"penalty","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getInitializedVersion","inputs":[],"outputs":[{"name":"","type":"uint64","internalType":"uint64"}],"stateMutability":"view"},{"type":"function","name":"getKeyRemovalCharge","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"keyRemovalCharge","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getKeysLimit","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"limit","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getMaxWithdrawalRequestFee","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"fee","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getPerformanceCoefficients","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"attestationsWeight","type":"uint256","internalType":"uint256"},{"name":"blocksWeight","type":"uint256","internalType":"uint256"},{"name":"syncWeight","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getPerformanceLeewayData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"data","type":"tuple[]","internalType":"struct ICSParametersRegistry.KeyNumberValueInterval[]","components":[{"name":"minKeyNumber","type":"uint256","internalType":"uint256"},{"name":"value","type":"uint256","internalType":"uint256"}]}],"stateMutability":"view"},{"type":"function","name":"getQueueConfig","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"queuePriority","type":"uint32","internalType":"uint32"},{"name":"maxDeposits","type":"uint32","internalType":"uint32"}],"stateMutability":"view"},{"type":"function","name":"getRewardShareData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"data","type":"tuple[]","internalType":"struct ICSParametersRegistry.KeyNumberValueInterval[]","components":[{"name":"minKeyNumber","type":"uint256","internalType":"uint256"},{"name":"value","type":"uint256","internalType":"uint256"}]}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"getStrikesParams","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"lifetime","type":"uint256","internalType":"uint256"},{"name":"threshold","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"initialize","inputs":[{"name":"admin","type":"address","internalType":"address"},{"name":"data","type":"tuple","internalType":"struct ICSParametersRegistry.InitializationData","components":[{"name":"keyRemovalCharge","type":"uint256","internalType":"uint256"},{"name":"elRewardsStealingAdditionalFine","type":"uint256","internalType":"uint256"},{"name":"keysLimit","type":"uint256","internalType":"uint256"},{"name":"rewardShare","type":"uint256","internalType":"uint256"},{"name":"performanceLeeway","type":"uint256","internalType":"uint256"},{"name":"strikesLifetime","type":"uint256","internalType":"uint256"},{"name":"strikesThreshold","type":"uint256","internalType":"uint256"},{"name":"defaultQueuePriority","type":"uint256","internalType":"uint256"},{"name":"defaultQueueMaxDeposits","type":"uint256","internalType":"uint256"},{"name":"badPerformancePenalty","type":"uint256","internalType":"uint256"},{"name":"attestationsWeight","type":"uint256","internalType":"uint256"},{"name":"blocksWeight","type":"uint256","internalType":"uint256"},{"name":"syncWeight","type":"uint256","internalType":"uint256"},{"name":"defaultAllowedExitDelay","type":"uint256","internalType":"uint256"},{"name":"defaultExitDelayPenalty","type":"uint256","internalType":"uint256"},{"name":"defaultMaxWithdrawalRequestFee","type":"uint256","internalType":"uint256"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setAllowedExitDelay","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"delay","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setBadPerformancePenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"penalty","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultAllowedExitDelay","inputs":[{"name":"delay","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultBadPerformancePenalty","inputs":[{"name":"penalty","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultElRewardsStealingAdditionalFine","inputs":[{"name":"fine","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultExitDelayPenalty","inputs":[{"name":"penalty","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultKeyRemovalCharge","inputs":[{"name":"keyRemovalCharge","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultKeysLimit","inputs":[{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultMaxWithdrawalRequestFee","inputs":[{"name":"fee","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultPerformanceCoefficients","inputs":[{"name":"attestationsWeight","type":"uint256","internalType":"uint256"},{"name":"blocksWeight","type":"uint256","internalType":"uint256"},{"name":"syncWeight","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultPerformanceLeeway","inputs":[{"name":"leeway","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultQueueConfig","inputs":[{"name":"priority","type":"uint256","internalType":"uint256"},{"name":"maxDeposits","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultRewardShare","inputs":[{"name":"share","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setDefaultStrikesParams","inputs":[{"name":"lifetime","type":"uint256","internalType":"uint256"},{"name":"threshold","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setElRewardsStealingAdditionalFine","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"fine","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setExitDelayPenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"penalty","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setKeyRemovalCharge","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"keyRemovalCharge","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setKeysLimit","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"limit","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setMaxWithdrawalRequestFee","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"fee","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setPerformanceCoefficients","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"attestationsWeight","type":"uint256","internalType":"uint256"},{"name":"blocksWeight","type":"uint256","internalType":"uint256"},{"name":"syncWeight","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setPerformanceLeewayData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"data","type":"tuple[]","internalType":"struct ICSParametersRegistry.KeyNumberValueInterval[]","components":[{"name":"minKeyNumber","type":"uint256","internalType":"uint256"},{"name":"value","type":"uint256","internalType":"uint256"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setQueueConfig","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"priority","type":"uint256","internalType":"uint256"},{"name":"maxDeposits","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setRewardShareData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"data","type":"tuple[]","internalType":"struct ICSParametersRegistry.KeyNumberValueInterval[]","components":[{"name":"minKeyNumber","type":"uint256","internalType":"uint256"},{"name":"value","type":"uint256","internalType":"uint256"}]}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setStrikesParams","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"},{"name":"lifetime","type":"uint256","internalType":"uint256"},{"name":"threshold","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"unsetAllowedExitDelay","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetBadPerformancePenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetElRewardsStealingAdditionalFine","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetExitDelayPenalty","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetKeyRemovalCharge","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetKeysLimit","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetMaxWithdrawalRequestFee","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetPerformanceCoefficients","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetPerformanceLeewayData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetQueueConfig","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetRewardShareData","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"unsetStrikesParams","inputs":[{"name":"curveId","type":"uint256","internalType":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"event","name":"AllowedExitDelaySet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"delay","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"AllowedExitDelayUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BadPerformancePenaltySet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"penalty","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"BadPerformancePenaltyUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultAllowedExitDelaySet","inputs":[{"name":"delay","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultBadPerformancePenaltySet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultElRewardsStealingAdditionalFineSet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultExitDelayPenaltySet","inputs":[{"name":"penalty","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultKeyRemovalChargeSet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultKeysLimitSet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultMaxWithdrawalRequestFeeSet","inputs":[{"name":"fee","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultPerformanceCoefficientsSet","inputs":[{"name":"attestationsWeight","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"blocksWeight","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"syncWeight","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultPerformanceLeewaySet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultQueueConfigSet","inputs":[{"name":"priority","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"maxDeposits","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultRewardShareSet","inputs":[{"name":"value","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"DefaultStrikesParamsSet","inputs":[{"name":"lifetime","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"threshold","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ElRewardsStealingAdditionalFineSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"fine","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ElRewardsStealingAdditionalFineUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ExitDelayPenaltySet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"penalty","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"ExitDelayPenaltyUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"KeyRemovalChargeSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"keyRemovalCharge","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"KeyRemovalChargeUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"KeysLimitSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"limit","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"KeysLimitUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"MaxWithdrawalRequestFeeSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"fee","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"MaxWithdrawalRequestFeeUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PerformanceCoefficientsSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"attestationsWeight","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"blocksWeight","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"syncWeight","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PerformanceCoefficientsUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"PerformanceLeewayDataSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"data","type":"tuple[]","indexed":false,"internalType":"struct ICSParametersRegistry.KeyNumberValueInterval[]","components":[{"name":"minKeyNumber","type":"uint256","internalType":"uint256"},{"name":"value","type":"uint256","internalType":"uint256"}]}],"anonymous":false},{"type":"event","name":"PerformanceLeewayDataUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"QueueConfigSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"priority","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"maxDeposits","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"QueueConfigUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"RewardShareDataSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"data","type":"tuple[]","indexed":false,"internalType":"struct ICSParametersRegistry.KeyNumberValueInterval[]","components":[{"name":"minKeyNumber","type":"uint256","internalType":"uint256"},{"name":"value","type":"uint256","internalType":"uint256"}]}],"anonymous":false},{"type":"event","name":"RewardShareDataUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"StrikesParamsSet","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"},{"name":"lifetime","type":"uint256","indexed":false,"internalType":"uint256"},{"name":"threshold","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"StrikesParamsUnset","inputs":[{"name":"curveId","type":"uint256","indexed":true,"internalType":"uint256"}],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"InvalidAllowedExitDelay","inputs":[]},{"type":"error","name":"InvalidExitDelayPenalty","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidKeyNumberValueIntervals","inputs":[]},{"type":"error","name":"InvalidPerformanceCoefficients","inputs":[]},{"type":"error","name":"InvalidPerformanceLeewayData","inputs":[]},{"type":"error","name":"InvalidRewardShareData","inputs":[]},{"type":"error","name":"InvalidStrikesParams","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"QueueCannotBeUsed","inputs":[]},{"type":"error","name":"SafeCastOverflowedUintDowncast","inputs":[{"name":"bits","type":"uint8","internalType":"uint8"},{"name":"value","type":"uint256","internalType":"uint256"}]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroMaxDeposits","inputs":[]},{"type":"error","name":"ZeroQueueLowestPriority","inputs":[]}] diff --git a/assets/CSStrikes.json b/assets/CSStrikes.json index 9f17ab33b..fdd452171 100644 --- a/assets/CSStrikes.json +++ b/assets/CSStrikes.json @@ -1 +1 @@ -[{"type":"constructor","inputs":[{"name":"module","type":"address","internalType":"address"},{"name":"oracle","type":"address","internalType":"address"},{"name":"exitPenalties","type":"address","internalType":"address"}],"stateMutability":"nonpayable"},{"type":"function","name":"ACCOUNTING","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSAccounting"}],"stateMutability":"view"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"EXIT_PENALTIES","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSExitPenalties"}],"stateMutability":"view"},{"type":"function","name":"MODULE","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSModule"}],"stateMutability":"view"},{"type":"function","name":"ORACLE","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"PARAMETERS_REGISTRY","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSParametersRegistry"}],"stateMutability":"view"},{"type":"function","name":"ejector","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSEjector"}],"stateMutability":"view"},{"type":"function","name":"getInitializedVersion","inputs":[],"outputs":[{"name":"","type":"uint64","internalType":"uint64"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"hashLeaf","inputs":[{"name":"keyStrikes","type":"tuple","internalType":"struct ICSStrikes.ModuleKeyStrikes","components":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"},{"name":"data","type":"uint256[]","internalType":"uint256[]"}]},{"name":"pubkey","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"pure"},{"type":"function","name":"initialize","inputs":[{"name":"admin","type":"address","internalType":"address"},{"name":"_ejector","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"processBadPerformanceProof","inputs":[{"name":"keyStrikesList","type":"tuple[]","internalType":"struct ICSStrikes.ModuleKeyStrikes[]","components":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"},{"name":"data","type":"uint256[]","internalType":"uint256[]"}]},{"name":"proof","type":"bytes32[]","internalType":"bytes32[]"},{"name":"proofFlags","type":"bool[]","internalType":"bool[]"},{"name":"refundRecipient","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"processOracleReport","inputs":[{"name":"_treeRoot","type":"bytes32","internalType":"bytes32"},{"name":"_treeCid","type":"string","internalType":"string"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setEjector","inputs":[{"name":"_ejector","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"treeCid","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"treeRoot","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"verifyProof","inputs":[{"name":"keyStrikesList","type":"tuple[]","internalType":"struct ICSStrikes.ModuleKeyStrikes[]","components":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"},{"name":"data","type":"uint256[]","internalType":"uint256[]"}]},{"name":"pubkeys","type":"bytes[]","internalType":"bytes[]"},{"name":"proof","type":"bytes32[]","internalType":"bytes32[]"},{"name":"proofFlags","type":"bool[]","internalType":"bool[]"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"event","name":"EjectorSet","inputs":[{"name":"ejector","type":"address","indexed":false,"internalType":"address"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"StrikesDataUpdated","inputs":[{"name":"treeRoot","type":"bytes32","indexed":false,"internalType":"bytes32"},{"name":"treeCid","type":"string","indexed":false,"internalType":"string"}],"anonymous":false},{"type":"event","name":"StrikesDataWiped","inputs":[],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidProof","inputs":[]},{"type":"error","name":"InvalidReportData","inputs":[]},{"type":"error","name":"MerkleProofInvalidMultiproof","inputs":[]},{"type":"error","name":"NotEnoughStrikesToEject","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"NotOracle","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroBadPerformancePenaltyAmount","inputs":[]},{"type":"error","name":"ZeroEjectionFeeAmount","inputs":[]},{"type":"error","name":"ZeroEjectorAddress","inputs":[]},{"type":"error","name":"ZeroExitPenaltiesAddress","inputs":[]},{"type":"error","name":"ZeroModuleAddress","inputs":[]},{"type":"error","name":"ZeroOracleAddress","inputs":[]}] \ No newline at end of file +[{"type":"constructor","inputs":[{"name":"module","type":"address","internalType":"address"},{"name":"oracle","type":"address","internalType":"address"},{"name":"exitPenalties","type":"address","internalType":"address"},{"name":"parametersRegistry","type":"address","internalType":"address"}],"stateMutability":"nonpayable"},{"type":"function","name":"ACCOUNTING","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSAccounting"}],"stateMutability":"view"},{"type":"function","name":"DEFAULT_ADMIN_ROLE","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"EXIT_PENALTIES","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSExitPenalties"}],"stateMutability":"view"},{"type":"function","name":"MODULE","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSModule"}],"stateMutability":"view"},{"type":"function","name":"ORACLE","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"PARAMETERS_REGISTRY","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSParametersRegistry"}],"stateMutability":"view"},{"type":"function","name":"ejector","inputs":[],"outputs":[{"name":"","type":"address","internalType":"contract ICSEjector"}],"stateMutability":"view"},{"type":"function","name":"getInitializedVersion","inputs":[],"outputs":[{"name":"","type":"uint64","internalType":"uint64"}],"stateMutability":"view"},{"type":"function","name":"getRoleAdmin","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"getRoleMember","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"index","type":"uint256","internalType":"uint256"}],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"getRoleMemberCount","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"grantRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"hasRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"hashLeaf","inputs":[{"name":"keyStrikes","type":"tuple","internalType":"struct ICSStrikes.KeyStrikes","components":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"},{"name":"data","type":"uint256[]","internalType":"uint256[]"}]},{"name":"pubkey","type":"bytes","internalType":"bytes"}],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"pure"},{"type":"function","name":"initialize","inputs":[{"name":"admin","type":"address","internalType":"address"},{"name":"_ejector","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"processBadPerformanceProof","inputs":[{"name":"keyStrikesList","type":"tuple[]","internalType":"struct ICSStrikes.KeyStrikes[]","components":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"},{"name":"data","type":"uint256[]","internalType":"uint256[]"}]},{"name":"proof","type":"bytes32[]","internalType":"bytes32[]"},{"name":"proofFlags","type":"bool[]","internalType":"bool[]"},{"name":"refundRecipient","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"payable"},{"type":"function","name":"processOracleReport","inputs":[{"name":"_treeRoot","type":"bytes32","internalType":"bytes32"},{"name":"_treeCid","type":"string","internalType":"string"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"renounceRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"callerConfirmation","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"revokeRole","inputs":[{"name":"role","type":"bytes32","internalType":"bytes32"},{"name":"account","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"setEjector","inputs":[{"name":"_ejector","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"supportsInterface","inputs":[{"name":"interfaceId","type":"bytes4","internalType":"bytes4"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"treeCid","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"treeRoot","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"verifyProof","inputs":[{"name":"keyStrikesList","type":"tuple[]","internalType":"struct ICSStrikes.KeyStrikes[]","components":[{"name":"nodeOperatorId","type":"uint256","internalType":"uint256"},{"name":"keyIndex","type":"uint256","internalType":"uint256"},{"name":"data","type":"uint256[]","internalType":"uint256[]"}]},{"name":"pubkeys","type":"bytes[]","internalType":"bytes[]"},{"name":"proof","type":"bytes32[]","internalType":"bytes32[]"},{"name":"proofFlags","type":"bool[]","internalType":"bool[]"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"event","name":"EjectorSet","inputs":[{"name":"ejector","type":"address","indexed":false,"internalType":"address"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"RoleAdminChanged","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"previousAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"newAdminRole","type":"bytes32","indexed":true,"internalType":"bytes32"}],"anonymous":false},{"type":"event","name":"RoleGranted","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"RoleRevoked","inputs":[{"name":"role","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"account","type":"address","indexed":true,"internalType":"address"},{"name":"sender","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"StrikesDataUpdated","inputs":[{"name":"treeRoot","type":"bytes32","indexed":false,"internalType":"bytes32"},{"name":"treeCid","type":"string","indexed":false,"internalType":"string"}],"anonymous":false},{"type":"event","name":"StrikesDataWiped","inputs":[],"anonymous":false},{"type":"error","name":"AccessControlBadConfirmation","inputs":[]},{"type":"error","name":"AccessControlUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"},{"name":"neededRole","type":"bytes32","internalType":"bytes32"}]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"InvalidProof","inputs":[]},{"type":"error","name":"InvalidReportData","inputs":[]},{"type":"error","name":"MerkleProofInvalidMultiproof","inputs":[]},{"type":"error","name":"NotEnoughStrikesToEject","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"SenderIsNotOracle","inputs":[]},{"type":"error","name":"ValueNotEvenlyDivisible","inputs":[]},{"type":"error","name":"ZeroAdminAddress","inputs":[]},{"type":"error","name":"ZeroBadPerformancePenaltyAmount","inputs":[]},{"type":"error","name":"ZeroEjectionFeeAmount","inputs":[]},{"type":"error","name":"ZeroEjectorAddress","inputs":[]},{"type":"error","name":"ZeroExitPenaltiesAddress","inputs":[]},{"type":"error","name":"ZeroModuleAddress","inputs":[]},{"type":"error","name":"ZeroOracleAddress","inputs":[]},{"type":"error","name":"ZeroParametersRegistryAddress","inputs":[]}] From 88f6290c2789dee9df1044fa237772394e48ff71 Mon Sep 17 00:00:00 2001 From: chasingrainbows Date: Fri, 23 May 2025 21:48:27 +0300 Subject: [PATCH 152/162] feat(csm-v2): speed up csm contracts load for fork tests --- tests/fork/conftest.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/fork/conftest.py b/tests/fork/conftest.py index ba2656eea..349b141be 100644 --- a/tests/fork/conftest.py +++ b/tests/fork/conftest.py @@ -94,6 +94,20 @@ def set_delay_and_sleep(monkeypatch): yield +@pytest.fixture(autouse=True) +def patch_csm_contract_load(monkeypatch): + monkeypatch.setattr( + "src.web3py.extensions.CSM.CONTRACT_LOAD_MAX_RETRIES", + 3, + ) + monkeypatch.setattr( + "src.web3py.extensions.CSM.CONTRACT_LOAD_RETRY_DELAY", + 0, + ) + logger.info("TESTRUN Patched CSM CONTRACT_LOAD_MAX_RETRIES to 3 and CONTRACT_LOAD_RETRY_DELAY to 0") + yield + + @pytest.fixture(autouse=True) def set_cache_path(monkeypatch, testrun_path): with monkeypatch.context(): From cfc3cfe0b72f1e74f19eb293c7ab0463d4a2d25d Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Fri, 23 May 2025 22:06:05 +0200 Subject: [PATCH 153/162] fix: json-stream base processing --- src/providers/http_provider.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/providers/http_provider.py b/src/providers/http_provider.py index 6ab543793..4af9c347f 100644 --- a/src/providers/http_provider.py +++ b/src/providers/http_provider.py @@ -188,12 +188,12 @@ def _get_without_fallbacks( logger.debug({'msg': response_fail_msg}) raise self.PROVIDER_EXCEPTION(response_fail_msg, status=response.status_code, text=response.text) - if stream: - # There's no guarantee the JSON is valid at this point. - json_response = json_stream_requests.load(response) - try: - json_response = response.json() + if stream: + # There's no guarantee the JSON is valid at this point. + json_response = json_stream_requests.load(response) + else: + json_response = response.json() except JSONDecodeError as error: response_fail_msg = ( f'Failed to decode JSON response from {complete_endpoint} with text: "{str(response.text)}"' From a64055e85166d6a522cb7bbd592bef2456cb17e1 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Fri, 23 May 2025 22:20:54 +0200 Subject: [PATCH 154/162] fix: add `xfail` for `test_lido_module_report` --- tests/fork/test_lido_oracle_cycle.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/fork/test_lido_oracle_cycle.py b/tests/fork/test_lido_oracle_cycle.py index d13c68bb4..d0543dd97 100644 --- a/tests/fork/test_lido_oracle_cycle.py +++ b/tests/fork/test_lido_oracle_cycle.py @@ -44,6 +44,7 @@ def missed_initial_frame(frame_config: FrameConfig): return [first_slot_of_epoch(i) for i in sequence(_from, _to)] +@pytest.mark.xfail(raises=AssertionError, reason="Test is expected to fail. Lido contracts are not updated yet.") @pytest.mark.fork @pytest.mark.parametrize( 'module', From ccaf3c8b822a0c642856791464654c212c1d45f8 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Mon, 26 May 2025 09:29:59 +0200 Subject: [PATCH 155/162] fix: use `skip` instead of `xfail` --- tests/fork/test_lido_oracle_cycle.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/fork/test_lido_oracle_cycle.py b/tests/fork/test_lido_oracle_cycle.py index d0543dd97..f3405b118 100644 --- a/tests/fork/test_lido_oracle_cycle.py +++ b/tests/fork/test_lido_oracle_cycle.py @@ -44,7 +44,6 @@ def missed_initial_frame(frame_config: FrameConfig): return [first_slot_of_epoch(i) for i in sequence(_from, _to)] -@pytest.mark.xfail(raises=AssertionError, reason="Test is expected to fail. Lido contracts are not updated yet.") @pytest.mark.fork @pytest.mark.parametrize( 'module', @@ -57,6 +56,11 @@ def missed_initial_frame(frame_config: FrameConfig): indirect=True, ) def test_lido_module_report(module, set_oracle_members, running_finalized_slots, account_from): + # Skip if consensus version is different + current_consensus_version = module.report_contract.get_consensus_version() + if current_consensus_version != module.COMPATIBLE_CONSENSUS_VERSION: + pytest.skip(f"Consensus version {current_consensus_version} does not match expected {module.COMPATIBLE_CONSENSUS_VERSION}") + assert module.report_contract.get_last_processing_ref_slot() == 0, "Last processing ref slot should be 0" members = set_oracle_members(count=2) From 5b1661960d5a6aef50407d26033e98984d96b7bb Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Mon, 26 May 2025 09:50:00 +0200 Subject: [PATCH 156/162] fix: intervals signature --- .../contracts/cs_parameters_registry.py | 21 +++++++++---------- .../contracts/test_cs_parameters_registry.py | 6 +++--- tests/modules/csm/test_csm_distribution.py | 16 +++++++------- 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/providers/execution/contracts/cs_parameters_registry.py b/src/providers/execution/contracts/cs_parameters_registry.py index 1a1eb9a94..1dc7b0631 100644 --- a/src/providers/execution/contracts/cs_parameters_registry.py +++ b/src/providers/execution/contracts/cs_parameters_registry.py @@ -1,9 +1,10 @@ import logging +from collections import UserList from dataclasses import dataclass from web3.types import BlockIdentifier -from src.constants import TOTAL_BASIS_POINTS, UINT256_MAX, ATTESTATIONS_WEIGHT, BLOCKS_WEIGHT, SYNC_WEIGHT +from src.constants import TOTAL_BASIS_POINTS, ATTESTATIONS_WEIGHT, BLOCKS_WEIGHT, SYNC_WEIGHT from src.modules.csm.state import ValidatorDuties from src.providers.execution.base_interface import ContractInterface from src.utils.cache import global_lru_cache as lru_cache @@ -47,14 +48,12 @@ class KeyNumberValueInterval: value: int -@dataclass -class KeyNumberValue: - intervals: list[KeyNumberValueInterval] +class KeyNumberValueIntervalList(UserList[KeyNumberValueInterval]): def get_for(self, key_number: int) -> float: if key_number < 1: raise ValueError("Key number should be greater than 1 or equal") - for interval in sorted(self.intervals, key=lambda x: x.minKeyNumber, reverse=True): + for interval in sorted(self, key=lambda x: x.minKeyNumber, reverse=True): if key_number >= interval.minKeyNumber: return interval.value / TOTAL_BASIS_POINTS raise ValueError(f"No value found for key number={key_number}") @@ -69,8 +68,8 @@ class StrikesParams: @dataclass class CurveParams: perf_coeffs: PerformanceCoefficients - perf_leeway_data: KeyNumberValue - reward_share_data: KeyNumberValue + perf_leeway_data: KeyNumberValueIntervalList + reward_share_data: KeyNumberValueIntervalList strikes_params: StrikesParams @@ -100,7 +99,7 @@ def get_reward_share_data( self, curve_id: int, block_identifier: BlockIdentifier = "latest", - ) -> KeyNumberValue: + ) -> KeyNumberValueIntervalList: """Returns reward share data for given node operator""" resp = self.functions.getRewardShareData(curve_id).call(block_identifier=block_identifier) @@ -111,14 +110,14 @@ def get_reward_share_data( "block_identifier": repr(block_identifier), } ) - return KeyNumberValue(intervals=[KeyNumberValueInterval(r.minKeyNumber, r.value) for r in resp.intervals]) + return KeyNumberValueIntervalList([KeyNumberValueInterval(r.minKeyNumber, r.value) for r in resp]) @lru_cache() def get_performance_leeway_data( self, curve_id: int, block_identifier: BlockIdentifier = "latest", - ) -> KeyNumberValue: + ) -> KeyNumberValueIntervalList: """Returns performance leeway data for given node operator""" resp = self.functions.getPerformanceLeewayData(curve_id).call(block_identifier=block_identifier) @@ -129,7 +128,7 @@ def get_performance_leeway_data( "block_identifier": repr(block_identifier), } ) - return KeyNumberValue(intervals=[KeyNumberValueInterval(r.minKeyNumber, r.value) for r in resp.intervals]) + return KeyNumberValueIntervalList([KeyNumberValueInterval(r.minKeyNumber, r.value) for r in resp]) @lru_cache() def get_strikes_params( diff --git a/tests/integration/contracts/test_cs_parameters_registry.py b/tests/integration/contracts/test_cs_parameters_registry.py index 2740019be..accd2eedd 100644 --- a/tests/integration/contracts/test_cs_parameters_registry.py +++ b/tests/integration/contracts/test_cs_parameters_registry.py @@ -3,7 +3,7 @@ from src.providers.execution.contracts.cs_parameters_registry import ( PerformanceCoefficients, - KeyNumberValue, + KeyNumberValueIntervalList, StrikesParams, ) from tests.integration.contracts.contract_utils import check_contract, check_is_instance_of @@ -16,8 +16,8 @@ def test_cs_parameters_registry(cs_params_contract, caplog): cs_params_contract, [ ("get_performance_coefficients", None, check_is_instance_of(PerformanceCoefficients)), - ("get_reward_share_data", None, check_is_instance_of(KeyNumberValue)), - ("get_performance_leeway_data", None, check_is_instance_of(KeyNumberValue)), + ("get_reward_share_data", None, check_is_instance_of(KeyNumberValueIntervalList)), + ("get_performance_leeway_data", None, check_is_instance_of(KeyNumberValueIntervalList)), ("get_strikes_params", None, check_is_instance_of(StrikesParams)), ], caplog, diff --git a/tests/modules/csm/test_csm_distribution.py b/tests/modules/csm/test_csm_distribution.py index c5340c129..492ecfaf0 100644 --- a/tests/modules/csm/test_csm_distribution.py +++ b/tests/modules/csm/test_csm_distribution.py @@ -17,7 +17,7 @@ PerformanceCoefficients, CurveParams, KeyNumberValueInterval, - KeyNumberValue, + KeyNumberValueIntervalList, ) from src.providers.execution.exceptions import InconsistentData from src.types import NodeOperatorId, EpochNumber, ValidatorIndex @@ -608,11 +608,11 @@ def test_calculate_distribution_handles_invalid_distribution_in_total(): side_effect=lambda no_id, _: { NodeOperatorId(5): CurveParams( strikes_params=..., - perf_leeway_data=KeyNumberValue( - intervals=[KeyNumberValueInterval(1, 1000), KeyNumberValueInterval(2, 2000)] + perf_leeway_data=KeyNumberValueIntervalList( + [KeyNumberValueInterval(1, 1000), KeyNumberValueInterval(2, 2000)] ), - reward_share_data=KeyNumberValue( - intervals=[KeyNumberValueInterval(1, 10000), KeyNumberValueInterval(2, 9000)] + reward_share_data=KeyNumberValueIntervalList( + [KeyNumberValueInterval(1, 10000), KeyNumberValueInterval(2, 9000)] ), perf_coeffs=PerformanceCoefficients(attestations_weight=1, blocks_weight=0, sync_weight=0), ), @@ -1161,13 +1161,13 @@ def test_performance_coefficients_calc_performance(attestation_perf, proposal_pe ) @pytest.mark.unit def test_interval_mapping_returns_correct_reward_share(intervals, key_index, expected): - reward_share = KeyNumberValue(intervals=intervals) + reward_share = KeyNumberValueIntervalList(intervals=intervals) assert reward_share.get_for(key_index) == expected @pytest.mark.unit def test_interval_mapping_raises_error_for_invalid_key_number(): - reward_share = KeyNumberValue( + reward_share = KeyNumberValueIntervalList( intervals=[KeyNumberValueInterval(1, 1000), KeyNumberValueInterval(11, 2000), KeyNumberValueInterval(21, 3000)] ) with pytest.raises(ValueError, match="Key number should be greater than 1 or equal"): @@ -1176,6 +1176,6 @@ def test_interval_mapping_raises_error_for_invalid_key_number(): @pytest.mark.unit def test_interval_mapping_raises_error_for_key_number_out_of_range(): - reward_share = KeyNumberValue(intervals=[KeyNumberValueInterval(11, 10000)]) + reward_share = KeyNumberValueIntervalList(intervals=[KeyNumberValueInterval(11, 10000)]) with pytest.raises(ValueError, match="No value found for key number=2"): reward_share.get_for(2) From e2b239c6896ad465e130e32019b854c99faf6c63 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Mon, 26 May 2025 09:51:44 +0200 Subject: [PATCH 157/162] fix: linter --- tests/fork/test_lido_oracle_cycle.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/fork/test_lido_oracle_cycle.py b/tests/fork/test_lido_oracle_cycle.py index f3405b118..30fad386e 100644 --- a/tests/fork/test_lido_oracle_cycle.py +++ b/tests/fork/test_lido_oracle_cycle.py @@ -59,7 +59,9 @@ def test_lido_module_report(module, set_oracle_members, running_finalized_slots, # Skip if consensus version is different current_consensus_version = module.report_contract.get_consensus_version() if current_consensus_version != module.COMPATIBLE_CONSENSUS_VERSION: - pytest.skip(f"Consensus version {current_consensus_version} does not match expected {module.COMPATIBLE_CONSENSUS_VERSION}") + pytest.skip( + f"Consensus version {current_consensus_version} does not match expected {module.COMPATIBLE_CONSENSUS_VERSION}" + ) assert module.report_contract.get_last_processing_ref_slot() == 0, "Last processing ref slot should be 0" members = set_oracle_members(count=2) From 9a9cc92ea9670d46b161b1a8e5727f70b17d63da Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Mon, 26 May 2025 10:07:14 +0200 Subject: [PATCH 158/162] fix: tests with intervals --- tests/modules/csm/test_csm_distribution.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/modules/csm/test_csm_distribution.py b/tests/modules/csm/test_csm_distribution.py index 492ecfaf0..b81682a11 100644 --- a/tests/modules/csm/test_csm_distribution.py +++ b/tests/modules/csm/test_csm_distribution.py @@ -1161,14 +1161,14 @@ def test_performance_coefficients_calc_performance(attestation_perf, proposal_pe ) @pytest.mark.unit def test_interval_mapping_returns_correct_reward_share(intervals, key_index, expected): - reward_share = KeyNumberValueIntervalList(intervals=intervals) + reward_share = KeyNumberValueIntervalList(intervals) assert reward_share.get_for(key_index) == expected @pytest.mark.unit def test_interval_mapping_raises_error_for_invalid_key_number(): reward_share = KeyNumberValueIntervalList( - intervals=[KeyNumberValueInterval(1, 1000), KeyNumberValueInterval(11, 2000), KeyNumberValueInterval(21, 3000)] + [KeyNumberValueInterval(1, 1000), KeyNumberValueInterval(11, 2000), KeyNumberValueInterval(21, 3000)] ) with pytest.raises(ValueError, match="Key number should be greater than 1 or equal"): reward_share.get_for(-1) @@ -1176,6 +1176,6 @@ def test_interval_mapping_raises_error_for_invalid_key_number(): @pytest.mark.unit def test_interval_mapping_raises_error_for_key_number_out_of_range(): - reward_share = KeyNumberValueIntervalList(intervals=[KeyNumberValueInterval(11, 10000)]) + reward_share = KeyNumberValueIntervalList([KeyNumberValueInterval(11, 10000)]) with pytest.raises(ValueError, match="No value found for key number=2"): reward_share.get_for(2) From a745896558b78fdee6d5bcf9bd15f0efe2dea255 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Mon, 26 May 2025 11:42:57 +0200 Subject: [PATCH 159/162] fix: env parsing for tests --- tests/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/__init__.py b/tests/__init__.py index 66b133d69..58d696a01 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -6,5 +6,5 @@ line = line.strip() if line.startswith("#") or not line: continue - key, value = line.split("=") + key, value = line.split("=", maxsplit=1) os.environ[key] = value From 1f02c4c1f31216a3e8e8e3f6ccda55717cbdac82 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Mon, 26 May 2025 13:31:54 +0200 Subject: [PATCH 160/162] feat: add `distributed_rewards` per-validator for logs --- src/modules/csm/distribution.py | 34 +++++++++++++++----- src/modules/csm/log.py | 3 +- src/providers/ipfs/public.py | 2 +- tests/modules/csm/test_csm_distribution.py | 37 ++++++++++++++-------- tests/modules/csm/test_log.py | 10 +++--- 5 files changed, 58 insertions(+), 28 deletions(-) diff --git a/src/modules/csm/distribution.py b/src/modules/csm/distribution.py index c2899ae95..9fb6771e1 100644 --- a/src/modules/csm/distribution.py +++ b/src/modules/csm/distribution.py @@ -10,7 +10,7 @@ from src.modules.csm.types import RewardsShares, StrikesList, StrikesValidator, ParticipationShares from src.providers.execution.contracts.cs_parameters_registry import PerformanceCoefficients from src.providers.execution.exceptions import InconsistentData -from src.types import EpochNumber, NodeOperatorId, ReferenceBlockStamp, StakingModuleAddress +from src.types import EpochNumber, NodeOperatorId, ReferenceBlockStamp, StakingModuleAddress, ValidatorIndex from src.utils.slot import get_reference_blockstamp from src.utils.web3converter import Web3Converter from src.web3py.extensions.lido_validators import LidoValidator, ValidatorsByNodeOperator @@ -124,7 +124,7 @@ def _calculate_distribution_in_frame( log: FramePerfLog, ) -> tuple[dict[NodeOperatorId, RewardsShares], RewardsShares, RewardsShares, dict[StrikesValidator, int]]: total_rebate_share = 0 - participation_shares: defaultdict[NodeOperatorId, ParticipationShares] = defaultdict(ParticipationShares) + participation_shares: dict[NodeOperatorId, dict[ValidatorIndex, ParticipationShares]] = {} frame_strikes: dict[StrikesValidator, int] = {} network_perf = self._get_network_performance(frame) @@ -160,11 +160,14 @@ def _calculate_distribution_in_frame( if validator_duties_outcome.strikes: frame_strikes[(no_id, validator.pubkey)] = validator_duties_outcome.strikes log_operator.validators[validator.index].strikes = validator_duties_outcome.strikes - participation_shares[no_id] += validator_duties_outcome.participation_share + if not participation_shares.get(no_id): + participation_shares[no_id] = {} + participation_shares[no_id][validator.index] = validator_duties_outcome.participation_share + total_rebate_share += validator_duties_outcome.rebate_share rewards_distribution_map = self.calc_rewards_distribution_in_frame( - participation_shares, total_rebate_share, rewards_to_distribute + participation_shares, total_rebate_share, rewards_to_distribute, log ) distributed_rewards = sum(rewards_distribution_map.values()) # All rewards to distribute should not be rebated if no duties were assigned to validators or @@ -172,7 +175,7 @@ def _calculate_distribution_in_frame( rebate_to_protocol = 0 if not distributed_rewards else rewards_to_distribute - distributed_rewards for no_id, no_rewards in rewards_distribution_map.items(): - log.operators[no_id].distributed = no_rewards + log.operators[no_id].distributed_rewards = no_rewards log.distributable = rewards_to_distribute log.distributed_rewards = distributed_rewards log.rebate_to_protocol = rebate_to_protocol @@ -247,22 +250,37 @@ def get_validator_duties_outcome( @staticmethod def calc_rewards_distribution_in_frame( - participation_shares: dict[NodeOperatorId, ParticipationShares], + participation_shares: dict[NodeOperatorId, dict[ValidatorIndex, ParticipationShares]], rebate_share: ParticipationShares, rewards_to_distribute: RewardsShares, + log: FramePerfLog, ) -> dict[NodeOperatorId, RewardsShares]: if rewards_to_distribute < 0: raise ValueError(f"Invalid rewards to distribute: {rewards_to_distribute=}") rewards_distribution: dict[NodeOperatorId, RewardsShares] = defaultdict(RewardsShares) - total_shares = rebate_share + sum(participation_shares.values()) - for no_id, no_participation_share in participation_shares.items(): + node_operators_participation_shares_sum = 0 + per_node_operator_participation_shares: dict[NodeOperatorId, ParticipationShares] = {} + for no_id, per_validator_participation_shares in participation_shares.items(): + no_participation_share = sum(per_validator_participation_shares.values()) if no_participation_share == 0: # Skip operators with zero participation continue + per_node_operator_participation_shares[no_id] = no_participation_share + node_operators_participation_shares_sum += no_participation_share + + total_shares = rebate_share + node_operators_participation_shares_sum + + for no_id, no_participation_share in per_node_operator_participation_shares.items(): rewards_distribution[no_id] = rewards_to_distribute * no_participation_share // total_shares + # Just for logging purpose. We don't expect here any accurate values. + for val_index, val_participation_share in participation_shares[no_id].items(): + log.operators[no_id].validators[val_index].distributed_rewards = ( + rewards_to_distribute * val_participation_share // total_shares + ) + return rewards_distribution @staticmethod diff --git a/src/modules/csm/log.py b/src/modules/csm/log.py index 997515ba7..3dfef5be3 100644 --- a/src/modules/csm/log.py +++ b/src/modules/csm/log.py @@ -13,6 +13,7 @@ class LogJSONEncoder(json.JSONEncoder): ... @dataclass class ValidatorFrameSummary: + distributed_rewards: RewardsShares = 0 performance: float = 0.0 threshold: float = 0.0 rewards_share: float = 0.0 @@ -25,7 +26,7 @@ class ValidatorFrameSummary: @dataclass class OperatorFrameSummary: - distributed: int = 0 + distributed_rewards: RewardsShares = 0 performance_coefficients: PerformanceCoefficients = field(default_factory=PerformanceCoefficients) validators: dict[ValidatorIndex, ValidatorFrameSummary] = field(default_factory=lambda: defaultdict(ValidatorFrameSummary)) diff --git a/src/providers/ipfs/public.py b/src/providers/ipfs/public.py index 17019bf26..c3eb92460 100644 --- a/src/providers/ipfs/public.py +++ b/src/providers/ipfs/public.py @@ -13,7 +13,7 @@ class PublicIPFS(IPFSProvider): # pylint:disable=duplicate-code - GATEWAY = "https://ipfs.io" + GATEWAY = "https://gateway.pinata.cloud" def __init__(self, *, timeout: int) -> None: super().__init__() diff --git a/tests/modules/csm/test_csm_distribution.py b/tests/modules/csm/test_csm_distribution.py index b81682a11..87b40a90c 100644 --- a/tests/modules/csm/test_csm_distribution.py +++ b/tests/modules/csm/test_csm_distribution.py @@ -20,7 +20,7 @@ KeyNumberValueIntervalList, ) from src.providers.execution.exceptions import InconsistentData -from src.types import NodeOperatorId, EpochNumber, ValidatorIndex +from src.types import NodeOperatorId, EpochNumber, ValidatorIndex, ReferenceBlockStamp from src.web3py.extensions import CSM from src.web3py.types import Web3 from tests.factory.blockstamp import ReferenceBlockStampFactory @@ -428,10 +428,11 @@ def test_calculate_distribution_handles_invalid_distribution_in_total(): rebate_to_protocol=0, operators={ NodeOperatorId(1): OperatorFrameSummary( - distributed=100, + distributed_rewards=100, performance_coefficients=PerformanceCoefficients(), validators={ ValidatorIndex(1): ValidatorFrameSummary( + distributed_rewards=100, performance=0.6, threshold=0.5, rewards_share=1.0, @@ -506,7 +507,7 @@ def test_calculate_distribution_handles_invalid_distribution_in_total(): rebate_to_protocol=0, operators={ NodeOperatorId(1): OperatorFrameSummary( - distributed=0, + distributed_rewards=0, performance_coefficients=PerformanceCoefficients(), validators={ ValidatorIndex(1): ValidatorFrameSummary( @@ -653,12 +654,13 @@ def test_calculate_distribution_handles_invalid_distribution_in_total(): OperatorFrameSummary, { NodeOperatorId(1): OperatorFrameSummary( - distributed=25, + distributed_rewards=25, performance_coefficients=PerformanceCoefficients(), validators=defaultdict( ValidatorFrameSummary, { ValidatorIndex(1): ValidatorFrameSummary( + distributed_rewards=25, performance=1.0, threshold=0.8842289719626168, rewards_share=1, @@ -673,12 +675,13 @@ def test_calculate_distribution_handles_invalid_distribution_in_total(): ), ), NodeOperatorId(2): OperatorFrameSummary( - distributed=25, + distributed_rewards=25, performance_coefficients=PerformanceCoefficients(), validators=defaultdict( ValidatorFrameSummary, { ValidatorIndex(3): ValidatorFrameSummary( + distributed_rewards=25, performance=1.0, threshold=0.8842289719626168, rewards_share=1, @@ -686,6 +689,7 @@ def test_calculate_distribution_handles_invalid_distribution_in_total(): sync_duty=DutyAccumulator(assigned=10, included=10), ), ValidatorIndex(4): ValidatorFrameSummary( + distributed_rewards=0, performance=0.12903225806451613, threshold=0.8842289719626168, rewards_share=1, @@ -697,7 +701,7 @@ def test_calculate_distribution_handles_invalid_distribution_in_total(): ), ), NodeOperatorId(3): OperatorFrameSummary( - distributed=0, + distributed_rewards=0, performance_coefficients=PerformanceCoefficients(), validators=defaultdict( ValidatorFrameSummary, @@ -713,7 +717,7 @@ def test_calculate_distribution_handles_invalid_distribution_in_total(): ), ), NodeOperatorId(5): OperatorFrameSummary( - distributed=47, + distributed_rewards=47, performance_coefficients=PerformanceCoefficients( attestations_weight=1, blocks_weight=0, @@ -723,6 +727,7 @@ def test_calculate_distribution_handles_invalid_distribution_in_total(): ValidatorFrameSummary, { ValidatorIndex(7): ValidatorFrameSummary( + distributed_rewards=25, performance=1.0, threshold=0.8842289719626168, rewards_share=1.0, @@ -732,6 +737,7 @@ def test_calculate_distribution_handles_invalid_distribution_in_total(): proposal_duty=DutyAccumulator(assigned=10, included=10), ), ValidatorIndex(8): ValidatorFrameSummary( + distributed_rewards=22, performance=1.0, threshold=0.7842289719626168, rewards_share=0.9, @@ -951,13 +957,13 @@ def test_process_validator_duty(validator_duties, is_slashed, threshold, reward_ "participation_shares, rewards_to_distribute, rebate_share, expected_distribution", [ ( - {NodeOperatorId(1): 100, NodeOperatorId(2): 200}, + {NodeOperatorId(1): {ValidatorIndex(0): 100}, NodeOperatorId(2): {ValidatorIndex(1): 200}}, Wei(1 * 10**18), 0, {NodeOperatorId(1): Wei(333333333333333333), NodeOperatorId(2): Wei(666666666666666666)}, ), ( - {NodeOperatorId(1): 0, NodeOperatorId(2): 0}, + {NodeOperatorId(1): {ValidatorIndex(0): 0}, NodeOperatorId(2): {ValidatorIndex(1): 0}}, Wei(1 * 10**18), 0, {}, @@ -969,13 +975,13 @@ def test_process_validator_duty(validator_duties, is_slashed, threshold, reward_ {}, ), ( - {NodeOperatorId(1): 100, NodeOperatorId(2): 0}, + {NodeOperatorId(1): {ValidatorIndex(0): 100}, NodeOperatorId(2): {ValidatorIndex(1): 0}}, Wei(1 * 10**18), 0, {NodeOperatorId(1): Wei(1 * 10**18)}, ), ( - {NodeOperatorId(1): 100, NodeOperatorId(2): 200}, + {NodeOperatorId(1): {ValidatorIndex(0): 100}, NodeOperatorId(2): {ValidatorIndex(1): 200}}, Wei(1 * 10**18), 10, {NodeOperatorId(1): Wei(322580645161290322), NodeOperatorId(2): Wei(645161290322580645)}, @@ -986,20 +992,23 @@ def test_process_validator_duty(validator_duties, is_slashed, threshold, reward_ def test_calc_rewards_distribution_in_frame( participation_shares, rewards_to_distribute, rebate_share, expected_distribution ): + log = FramePerfLog(ReferenceBlockStampFactory.build(), (EpochNumber(100), EpochNumber(500))) rewards_distribution = Distribution.calc_rewards_distribution_in_frame( - participation_shares, rebate_share, rewards_to_distribute + participation_shares, rebate_share, rewards_to_distribute, log ) assert rewards_distribution == expected_distribution @pytest.mark.unit def test_calc_rewards_distribution_in_frame_handles_negative_to_distribute(): - participation_shares = {NodeOperatorId(1): 100, NodeOperatorId(2): 200} + participation_shares = {NodeOperatorId(1): {ValidatorIndex(0): 100}, NodeOperatorId(2): {ValidatorIndex(1): 200}} rewards_to_distribute = Wei(-1) rebate_share = 0 with pytest.raises(ValueError, match="Invalid rewards to distribute"): - Distribution.calc_rewards_distribution_in_frame(participation_shares, rebate_share, rewards_to_distribute) + Distribution.calc_rewards_distribution_in_frame( + participation_shares, rebate_share, rewards_to_distribute, log=Mock() + ) @pytest.mark.parametrize( diff --git a/tests/modules/csm/test_log.py b/tests/modules/csm/test_log.py index e06fda4dd..c8fd64c76 100644 --- a/tests/modules/csm/test_log.py +++ b/tests/modules/csm/test_log.py @@ -30,7 +30,7 @@ def test_fields_access(log: FramePerfLog): @pytest.mark.unit def test_logs_encode(log: FramePerfLog): # Fill in dynamic fields to make sure we have data in it to be encoded. - log.operators[NodeOperatorId(42)].distributed = 17 + log.operators[NodeOperatorId(42)].distributed_rewards = 17 log.operators[NodeOperatorId(42)].performance_coefficients = PerformanceCoefficients() log.operators[NodeOperatorId(42)].validators["41337"].attestation_duty = DutyAccumulator(220, 119) log.operators[NodeOperatorId(42)].validators["41337"].proposal_duty = DutyAccumulator(1, 1) @@ -38,8 +38,9 @@ def test_logs_encode(log: FramePerfLog): log.operators[NodeOperatorId(42)].validators["41337"].performance = 0.5 log.operators[NodeOperatorId(42)].validators["41337"].threshold = 0.7 log.operators[NodeOperatorId(42)].validators["41337"].rewards_share = 0.3 + log.operators[NodeOperatorId(42)].validators["41337"].distributed_rewards = 17 - log.operators[NodeOperatorId(0)].distributed = 0 + log.operators[NodeOperatorId(0)].distributed_rewards = 0 log.operators[NodeOperatorId(0)].performance_coefficients = PerformanceCoefficients(1, 2, 3) log.distributable = 100 @@ -70,14 +71,15 @@ def test_logs_encode(log: FramePerfLog): assert decoded["operators"]["42"]["validators"]["41337"]["threshold"] == 0.7 assert decoded["operators"]["42"]["validators"]["41337"]["rewards_share"] == 0.3 assert decoded["operators"]["42"]["validators"]["41337"]["slashed"] == False - assert decoded["operators"]["42"]["distributed"] == 17 + assert decoded["operators"]["42"]["validators"]["41337"]["distributed_rewards"] == 17 + assert decoded["operators"]["42"]["distributed_rewards"] == 17 assert decoded["operators"]["42"]["performance_coefficients"] == { 'attestations_weight': 54, 'blocks_weight': 8, 'sync_weight': 2, } - assert decoded["operators"]["0"]["distributed"] == 0 + assert decoded["operators"]["0"]["distributed_rewards"] == 0 assert decoded["operators"]["0"]["performance_coefficients"] == { 'attestations_weight': 1, 'blocks_weight': 2, From 5bcb0882bd98e75ee2cef752221ee3cdb2bdccb5 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Mon, 26 May 2025 14:09:32 +0200 Subject: [PATCH 161/162] revert: url --- src/providers/ipfs/public.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/providers/ipfs/public.py b/src/providers/ipfs/public.py index c3eb92460..17019bf26 100644 --- a/src/providers/ipfs/public.py +++ b/src/providers/ipfs/public.py @@ -13,7 +13,7 @@ class PublicIPFS(IPFSProvider): # pylint:disable=duplicate-code - GATEWAY = "https://gateway.pinata.cloud" + GATEWAY = "https://ipfs.io" def __init__(self, *, timeout: int) -> None: super().__init__() From 1e768590fe3b6928d9d0a31751ebd1ef974cb557 Mon Sep 17 00:00:00 2001 From: vgorkavenko Date: Mon, 26 May 2025 16:16:33 +0200 Subject: [PATCH 162/162] chore: remove branch `feat/oracle-v6` from workflow trigger --- .github/workflows/mainnet_fork_tests.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/mainnet_fork_tests.yml b/.github/workflows/mainnet_fork_tests.yml index 97bd5fafe..84fd481e7 100644 --- a/.github/workflows/mainnet_fork_tests.yml +++ b/.github/workflows/mainnet_fork_tests.yml @@ -11,7 +11,6 @@ on: branches: - main - develop - - feat/oracle-v6 paths: - "src/**"