Skip to content

Commit

Permalink
Merge pull request #570 from lidofinance/7251-support-compound-wc
Browse files Browse the repository at this point in the history
Support compound WC
  • Loading branch information
F4ever authored Dec 11, 2024
2 parents 8cdfaee + a704b73 commit 2e9e7b9
Show file tree
Hide file tree
Showing 9 changed files with 231 additions and 47 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ FROM python:3.12.4-slim as base
RUN apt-get update && apt-get install -y --no-install-recommends -qq \
libffi-dev=3.4.4-1 \
g++=4:12.2.0-3 \
curl=7.88.1-10+deb12u7 \
curl=7.88.1-10+deb12u8 \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "lido-oracle"
version = "4.0.3"
version = "4.1.0"
description = "Oracle daemon for Lido decentralized staking service. Collects and reports Ethereum 2.0 beacon chain states (the number of visible validators and their summarized balances) to the Lido dApp contract running on Ethereum 1.0 side."
authors = [
"Dmitry Chernukhin",
Expand Down
12 changes: 10 additions & 2 deletions src/constants.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#misc
from src.types import Gwei

FAR_FUTURE_EPOCH = 2 ** 64 - 1
# https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#time-parameters-1
MIN_VALIDATOR_WITHDRAWABILITY_DELAY = 2**8
MIN_VALIDATOR_WITHDRAWABILITY_DELAY = 2 ** 8
SHARD_COMMITTEE_PERIOD = 256
MAX_SEED_LOOKAHEAD = 4
# https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#state-list-lengths
EPOCHS_PER_SLASHINGS_VECTOR = 2**13
EPOCHS_PER_SLASHINGS_VECTOR = 2 ** 13
# https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#rewards-and-penalties
PROPORTIONAL_SLASHING_MULTIPLIER_BELLATRIX = 3
# https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#gwei-values
Expand All @@ -15,12 +17,18 @@
MAX_WITHDRAWALS_PER_PAYLOAD = 2 ** 4
# https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#withdrawal-prefixes
ETH1_ADDRESS_WITHDRAWAL_PREFIX = '0x01'
# https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#withdrawal-prefixes
COMPOUNDING_WITHDRAWAL_PREFIX = '0x02'
# https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator-cycle
MIN_PER_EPOCH_CHURN_LIMIT = 2 ** 2
CHURN_LIMIT_QUOTIENT = 2 ** 16
# https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#time-parameters
SLOTS_PER_HISTORICAL_ROOT = 8192

# https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#gwei-values
MIN_ACTIVATION_BALANCE = Gwei(2 ** 5 * 10 ** 9)
MAX_EFFECTIVE_BALANCE_ELECTRA = Gwei(2 ** 11 * 10 ** 9)

# Local constants
GWEI_TO_WEI = 10 ** 9
SHARE_RATE_PRECISION_E27 = 10 ** 27
Expand Down
11 changes: 7 additions & 4 deletions src/modules/csm/csm.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,10 +308,13 @@ def stuck_operators(self, blockstamp: ReferenceBlockStamp) -> set[NodeOperatorId
blockstamp.slot_number,
)
)
digests = self.w3.lido_validators.get_lido_node_operators_by_modules(l_blockstamp).get(self.module_id)
if digests is None:
raise InconsistentData(f"No Node Operators digests found for {self.module_id=}")
stuck.update(no.id for no in digests if no.stuck_validators_count > 0)

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:
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,
Expand Down
44 changes: 23 additions & 21 deletions src/modules/submodules/consensus.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,8 @@
from src.utils.cache import global_lru_cache as lru_cache
from src.web3py.types import Web3


logger = logging.getLogger(__name__)


# Initial epoch is in the future. Revert signature: '0xcd0883ea'
InitialEpochIsYetToArriveRevert = Web3.keccak(text="InitialEpochIsYetToArrive()")[:4].hex()

Expand Down Expand Up @@ -135,25 +133,29 @@ def get_member_info(self, blockstamp: BlockStamp) -> MemberInfo:
current_frame_consensus_report = current_frame_member_report = ZERO_HASH

if variables.ACCOUNT:
(
# Current frame's reference slot.
_, # current_frame_ref_slot
# Consensus report for the current frame, if any. Zero bytes otherwise.
current_frame_consensus_report,
# Whether the provided address is a member of the oracle committee.
is_member,
# Whether the oracle committee member is in the fast line members subset of the current reporting frame.
is_fast_lane,
# Whether the oracle committee member is allowed to submit a report at the moment of the call.
_, # can_report
# The last reference slot for which the member submitted a report.
last_member_report_ref_slot,
# The hash reported by the member for the current frame, if any.
current_frame_member_report,
) = consensus_contract.get_consensus_state_for_member(
variables.ACCOUNT.address,
blockstamp.block_hash,
)
try:
(
# Current frame's reference slot.
_, # current_frame_ref_slot
# Consensus report for the current frame, if any. Zero bytes otherwise.
current_frame_consensus_report,
# Whether the provided address is a member of the oracle committee.
is_member,
# Whether the oracle committee member is in the fast line members subset of the current reporting frame.
is_fast_lane,
# Whether the oracle committee member is allowed to submit a report at the moment of the call.
_, # can_report
# The last reference slot for which the member submitted a report.
last_member_report_ref_slot,
# The hash reported by the member for the current frame, if any.
current_frame_member_report,
) = consensus_contract.get_consensus_state_for_member(
variables.ACCOUNT.address,
blockstamp.block_hash,
)
except ContractCustomError as revert:
if revert.data != InitialEpochIsYetToArriveRevert:
raise revert

is_submit_member = self._is_submit_member(blockstamp)

Expand Down
41 changes: 35 additions & 6 deletions src/utils/validator_state.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
from typing import Sequence

from src.constants import (
MAX_EFFECTIVE_BALANCE,
ETH1_ADDRESS_WITHDRAWAL_PREFIX,
SHARD_COMMITTEE_PERIOD,
FAR_FUTURE_EPOCH,
EFFECTIVE_BALANCE_INCREMENT,
MAX_SEED_LOOKAHEAD,
MIN_PER_EPOCH_CHURN_LIMIT,
CHURN_LIMIT_QUOTIENT,
COMPOUNDING_WITHDRAWAL_PREFIX,
MAX_EFFECTIVE_BALANCE_ELECTRA,
MIN_ACTIVATION_BALANCE,
)
from src.providers.consensus.types import Validator
from src.types import EpochNumber, Gwei
Expand Down Expand Up @@ -41,15 +43,24 @@ def is_partially_withdrawable_validator(validator: Validator) -> bool:
Check if `validator` is partially withdrawable
https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#is_partially_withdrawable_validator
"""
has_max_effective_balance = int(validator.validator.effective_balance) == MAX_EFFECTIVE_BALANCE
has_excess_balance = int(validator.balance) > MAX_EFFECTIVE_BALANCE
max_effective_balance = get_max_effective_balance(validator)
has_max_effective_balance = int(validator.validator.effective_balance) == max_effective_balance
has_excess_balance = int(validator.balance) > max_effective_balance
return (
has_eth1_withdrawal_credential(validator)
has_execution_withdrawal_credential(validator)
and has_max_effective_balance
and has_excess_balance
)


def has_compounding_withdrawal_credential(validator: Validator) -> bool:
"""
Check if ``validator`` has an 0x02 prefixed "compounding" withdrawal credential.
https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#new-has_compounding_withdrawal_credential
"""
return validator.validator.withdrawal_credentials[:4] == COMPOUNDING_WITHDRAWAL_PREFIX


def has_eth1_withdrawal_credential(validator: Validator) -> bool:
"""
Check if ``validator`` has an 0x01 prefixed "eth1" withdrawal credential.
Expand All @@ -58,13 +69,21 @@ def has_eth1_withdrawal_credential(validator: Validator) -> bool:
return validator.validator.withdrawal_credentials[:4] == ETH1_ADDRESS_WITHDRAWAL_PREFIX


def has_execution_withdrawal_credential(validator: Validator) -> bool:
"""
Check if ``validator`` has a 0x01 or 0x02 prefixed withdrawal credential.
https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#new-has_execution_withdrawal_credential
"""
return has_compounding_withdrawal_credential(validator) or has_eth1_withdrawal_credential(validator)


def is_fully_withdrawable_validator(validator: Validator, epoch: EpochNumber) -> bool:
"""
Check if `validator` is fully withdrawable
https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#is_fully_withdrawable_validator
https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#modified-is_fully_withdrawable_validator
"""
return (
has_eth1_withdrawal_credential(validator)
has_execution_withdrawal_credential(validator)
and EpochNumber(int(validator.validator.withdrawable_epoch)) <= epoch
and Gwei(int(validator.balance)) > Gwei(0)
)
Expand Down Expand Up @@ -114,3 +133,13 @@ def compute_activation_exit_epoch(ref_epoch: EpochNumber):

def compute_exit_churn_limit(active_validators_count: int):
return max(MIN_PER_EPOCH_CHURN_LIMIT, active_validators_count // CHURN_LIMIT_QUOTIENT)


def get_max_effective_balance(validator: Validator) -> Gwei:
"""
Get max effective balance for ``validator``.
https://github.com/ethereum/consensus-specs/blob/dev/specs/electra/beacon-chain.md#new-get_max_effective_balance
"""
if has_compounding_withdrawal_credential(validator):
return MAX_EFFECTIVE_BALANCE_ELECTRA
return MIN_ACTIVATION_BALANCE
51 changes: 50 additions & 1 deletion tests/modules/csm/test_csm_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from src.providers.ipfs import CIDv0, CID
from src.types import EpochNumber, NodeOperatorId, SlotNumber, StakingModuleId, ValidatorIndex
from src.web3py.extensions.csm import CSM
from tests.factory.blockstamp import ReferenceBlockStampFactory
from tests.factory.blockstamp import BlockStampFactory, ReferenceBlockStampFactory
from tests.factory.configs import ChainConfigFactory, FrameConfigFactory


Expand Down Expand Up @@ -83,6 +83,55 @@ def test_stuck_operators(module: CSOracle, csm: CSM):
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):
module.module = Mock() # type: ignore
module.module_id = StakingModuleId(3)
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.csm.get_operators_with_stucks_in_range = Mock(
return_value=[
NodeOperatorId(2),
NodeOperatorId(4),
NodeOperatorId(6),
]
)

module.current_frame_range = 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.stuck_operators(blockstamp=blockstamp)

assert stuck == {
NodeOperatorId(2),
NodeOperatorId(4),
NodeOperatorId(6),
}

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)
Expand Down
28 changes: 25 additions & 3 deletions tests/modules/submodules/consensus/test_consensus.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,19 +143,41 @@ def test_get_frame_custom_exception(web3, consensus):
consensus.get_initial_or_current_frame(bs)


@pytest.fixture()
def use_account(request):
if request.param:
request.getfixturevalue("set_submit_account")
else:
request.getfixturevalue("set_no_account")


@pytest.mark.unit
def test_first_frame_is_not_yet_started(web3, consensus, caplog, set_no_account):
@pytest.mark.parametrize(
"use_account", [True, False], ids=['with_account', "without_account"], indirect=["use_account"]
)
def test_first_frame_is_not_yet_started(web3, consensus, caplog, use_account):
bs = ReferenceBlockStampFactory.build()

consensus_contract = Mock(get_current_frame=Mock(side_effect=ContractCustomError('0xcd0883ea', '0xcd0883ea')))
err = ContractCustomError('0xcd0883ea', '0xcd0883ea')
consensus_contract = Mock(
get_current_frame=Mock(side_effect=err), get_consensus_state_for_member=Mock(side_effect=err)
)
consensus._get_consensus_contract = Mock(return_value=consensus_contract)
consensus._is_submit_member = Mock(return_value=True)
consensus.get_frame_config = Mock(return_value=FrameConfigFactory.build(initial_epoch=5, epochs_per_frame=10))
consensus.get_chain_config = Mock(return_value=ChainConfigFactory.build())

first_frame = consensus.get_initial_or_current_frame(bs)
member_info = consensus.get_member_info(bs)

assert first_frame.ref_slot == 5 * 32 - 1
assert first_frame.report_processing_deadline_slot == (5 + 10) * 32 - 1
assert member_info.is_submit_member
assert member_info.is_report_member
assert member_info.is_fast_lane
assert member_info.current_frame_consensus_report == ZERO_HASH
assert member_info.current_frame_member_report == ZERO_HASH
assert member_info.current_frame_ref_slot == first_frame.ref_slot
assert member_info.deadline_slot == first_frame.report_processing_deadline_slot


@pytest.mark.unit
Expand Down
Loading

0 comments on commit 2e9e7b9

Please sign in to comment.