Skip to content

Conversation

@F4ever
Copy link
Member

@F4ever F4ever commented Nov 7, 2025

Description

Describe what this pull request does. Explain the purpose of the changes and the problem they solve.

Related Issue/Task

How Has This Been Tested?

Describe how you tested the changes:

  • Local tests (e.g., pytest)
  • Manual testing (describe steps)
  • Not tested (explain why)

Checklist

  • Documentation updated (if required)
  • New tests added (if applicable)
  • CSM_STATE_VERSION is bumped (if the new version affects data in the cache)

chasingrainbows and others added 30 commits June 25, 2025 14:17
"""
Returns the Vaults
"""
response = self.functions.batchVaultsInfo(offset, limit).call(block_identifier=block_identifier)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
response = self.functions.batchVaultsInfo(offset, limit).call(block_identifier=block_identifier)
response = self.functions().batchVaultsInfo(offset, limit).call(block_identifier=block_identifier)

CACHE_PATH: Final = Path(os.getenv("CACHE_PATH", "."))

VAULT_PAGINATION_LIMIT: Final = int(os.getenv("VAULT_PAGINATION_LIMIT", 1_000))
VAULT_VALIDATOR_STAGES_BATCH_SIZE: Final = int(os.getenv("VAULT_VALIDATOR_STAGES_BATCH_SIZE", 1_00))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tamtamchik you've not renamed this variable stages -> statuses
also formatting suggestion (just in case is it supposed to be 100 or 1_000?)

Suggested change
VAULT_VALIDATOR_STAGES_BATCH_SIZE: Final = int(os.getenv("VAULT_VALIDATOR_STAGES_BATCH_SIZE", 1_00))
VAULT_VALIDATOR_STAGES_BATCH_SIZE: Final = int(os.getenv("VAULT_VALIDATOR_STAGES_BATCH_SIZE", 100))


CACHE_PATH: Final = Path(os.getenv("CACHE_PATH", "."))

VAULT_PAGINATION_LIMIT: Final = int(os.getenv("VAULT_PAGINATION_LIMIT", 1_000))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too high. Estimated gas consumtion > 80kk. I propose to set it in 100.

CACHE_PATH: Final = Path(os.getenv("CACHE_PATH", "."))

VAULT_PAGINATION_LIMIT: Final = int(os.getenv("VAULT_PAGINATION_LIMIT", 1_000))
VAULT_VALIDATOR_STAGES_BATCH_SIZE: Final = int(os.getenv("VAULT_VALIDATOR_STAGES_BATCH_SIZE", 1_00))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
VAULT_VALIDATOR_STAGES_BATCH_SIZE: Final = int(os.getenv("VAULT_VALIDATOR_STAGES_BATCH_SIZE", 1_00))
VAULT_VALIDATOR_STAGES_BATCH_SIZE: Final = int(os.getenv("VAULT_VALIDATOR_STAGES_BATCH_SIZE", 100))

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be increased to 1_000

def get_validator_statuses(
self,
pubkeys: list[str],
batch_size: int = variables.VAULT_VALIDATOR_STAGES_BATCH_SIZE,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better provide this variable from staking vault service. Do not use as default, like this

out: dict[str, ValidatorStatus] = {}

for i in range(0, len(pubkeys), batch_size):
batch = list(map(HexBytes, pubkeys[i : i + batch_size]))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we applying HexBytes here? Let's remove it

for pk, status in zip(batch, response)
})

return out
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add info log after all responses were received

batch = self.get_vaults(
block_identifier=block_identifier, offset=offset, limit=variables.VAULT_PAGINATION_LIMIT
)
if not batch:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a case when we can receive empty response here?


return out

def get_all_vaults(self, block_identifier: BlockIdentifier = 'latest') -> list[VaultInfo]:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need tests for pagination in get_all_vaults and for get_validator_statuses


logger.info(
{
'msg': 'Call `simulateOracleReport({}, {}, {}, {}, {}, {}, {}, {}, {})`.'.format( # pylint: disable=consider-using-f-string
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
'msg': 'Call `simulateOracleReport({}, {}, {}, {}, {}, {}, {}, {}, {})`.'.format( # pylint: disable=consider-using-f-string
'msg': 'Call `simulateOracleReport({}, {}, {}, {}, {}, {}, {}, {}, {})`.'.format(*payload.as_tuple())


response = self.functions.simulateOracleReport(payload.as_tuple()).call(block_identifier=block_identifier)

response = ReportSimulationResults(*response)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better assign field by field:
eg balance=balance

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or use named_tuple_to_dataclass


out: list[VaultInfo] = []
for vault in response:
out.append(VaultInfo(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we use named_tuple_to_dataclass here?

Copy link
Member Author

@F4ever F4ever Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or just

@list_of_dataclasses(VaultInfo)

) -> list[MintedSharesOnVaultEvent]:
logs = get_events_in_range(self.events.MintedSharesOnVault(), from_block_number, to_block_number)

return [MintedSharesOnVaultEvent.from_log(log) for log in logs]
Copy link
Member Author

@F4ever F4ever Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need some tests on all from_log methods

Comment on lines +15 to +16
BRIDGE_URL = 'https://up.storacha.network/bridge'
GATEWAY_URL = 'https://storacha.link/ipfs/'
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe move this to variables?

API_GET_VALIDATORS = 'eth/v1/beacon/states/{}/validators'
API_GET_SPEC = 'eth/v1/config/spec'
API_GET_GENESIS = 'eth/v1/beacon/genesis'
API_GET_VALIDATOR = 'eth/v1/beacon/states/{}/validators/{}'
Copy link
Member Author

@F4ever F4ever Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add additional check with this api to checks module

'simulate_oracle_report',
(
ReportSimulationPayload(
timestamp=0,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we write a test with non zero vaules?

"""
validators_by_vault = self._get_validators_by_vault(validators, vaults)
total_pending_amount_by_pubkey = self._get_total_pending_amount_by_pubkey(pending_deposits)
inactive_validator_statuses = self._get_non_activated_validator_stages(validators, vaults, block_identifier)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_get_non_activated_validator_stages -> _get_non_activated_validator_statuses

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe get_non_aligable_to_activate_validators?

and v.validator.withdrawal_credentials in vault_wcs
]

return self.w3.lido_contracts.lazy_oracle.get_validator_statuses(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I propose to filter all not found keys here

return {pubkey: Gwei(deposits[pubkey]) for pubkey in deposits}

@staticmethod
def _get_validators_by_vault(validators: list[Validator], vaults: VaultsMap) -> VaultToValidators:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add unit tests for this func



def calculate_gross_core_apr(
post_internal_ether: int,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think here should be weis and shares everywhere

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: check why typecheking not working

from src.modules.accounting.types import SECONDS_IN_YEAR, Shares


def calculate_gross_core_apr(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it's net apr since we're removing fees from calculations?

rate_diff: Decimal = post_share_rate_no_fees - pre_share_rate

if rate_diff < 0:
return Decimal(0)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why it can't be negative?

time_elapsed_seconds=self._get_time_elapsed_seconds_from_prev_report(blockstamp),
)

latest_onchain_ipfs_report_data = self.staking_vaults.get_latest_onchain_ipfs_report_data(blockstamp.block_hash)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe call self.w3.lido_contracts.lazy_oracle.get_latest_report_data(blockstamp.block_hash) directly here. Function with one call is redundant

latest_onchain_ipfs_report_data: OnChainIpfsVaultReportData,
core_apr_ratio: Decimal,
pre_total_pooled_ether: int,
pre_total_shares: int,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pre_total_shares: int,
pre_total_shares: Shares,

bb = self.w3.ipfs.fetch(CID(tree_cid), current_frame)
return StakingVaultIpfsReport.parse_merkle_tree_data(bb)

def get_latest_onchain_ipfs_report_data(self, block_identifier: BlockIdentifier) -> OnChainIpfsVaultReportData:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets remove this function. Since and replace with direct contract call. Also report is not from IPFS, so the name confuses a little bit

chain_config: ChainConfig,
current_frame: FrameNumber,
) -> tuple[Optional[StakingVaultIpfsReport], BlockNumber]:
slots_per_frame = frame_config.epochs_per_frame * chain_config.slots_per_epoch
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Web3Converter can be reused to calculate slots and other staff

accounting_oracle = self.w3.lido_contracts.accounting_oracle

if latest_onchain_ipfs_report_data.report_cid != "":
prev_ipfs_report = self.get_ipfs_report(latest_onchain_ipfs_report_data.report_cid, current_frame)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we remove part with current frame from ipfs methods?

prev_ipfs_report = self.get_ipfs_report(latest_onchain_ipfs_report_data.report_cid, current_frame)
tree_root_hex = Web3.to_hex(latest_onchain_ipfs_report_data.tree_root)

if not self.is_tree_root_valid(tree_root_hex, prev_ipfs_report):
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we validate data in ipfs provider, so we can correctly use fallbacks in case in one of provider got broken

return output

def get_ipfs_report(self, ipfs_report_cid: str, current_frame: FrameNumber) -> StakingVaultIpfsReport:
if ipfs_report_cid == "":
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we already called this method in function below.
I propose to remove this function and call self.get_vault_report directly from _get_start_point_for_fee_calculations

current_frame: FrameNumber,
) -> tuple[Optional[StakingVaultIpfsReport], BlockNumber]:
slots_per_frame = frame_config.epochs_per_frame * chain_config.slots_per_epoch
accounting_oracle = self.w3.lido_contracts.accounting_oracle
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's inline this line into line 511

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants