本文档详细描述了EigenLayer奖励计算的流程,该过程通过一系列SQL查询来计算AVS向stakers和operators分配奖励。计算分为数据提取、数据转换和奖励计算三个阶段,每日运行,以确保与链上状态的一致性。文档还深入探讨了关键的SQL查询,涉及staker和operator的状态、奖励提交以及快照和窗口处理,最终生成Merkle化的奖励分配结果。
先前奖励计算的链接是 here.
EigenLayer 奖励计算是一组 SQL 查询,用于计算通过 RewardsCoordinator
由 AVS 向 staker 和 operator 分配的奖励。 数据被导入到 Postgres 表中并进行转换,以计算最终奖励。
这些查询在一个每日任务中运行,该任务使用核心合约状态的快照来计算来自任何活跃奖励提交的奖励。
该计算分 3 个阶段进行,并且每天运行
然后,该管道汇总所有奖励,直到 lastRewardTimestamp + calculationIntervalSeconds
,并提交一个将每个 earner 的累积总和进行 Merkle 化处理的根,提交到 RewardsCoordinator
。
奖励计算是一个 airflow 管道,每天 UTC 时间 16:00 运行。 对链上事件和事件计算的查询都向下舍入到 UTC 时间 0:00。 也就是说,如果管道在 4 月 27 日 UTC 时间 16:00 运行,则 cutoff_date
参数设置为 4 月 26 日 UTC 时间 0:00。
我们通过在 UTC 时间 0:00 之后几个小时运行每日管道来处理重组,从而为我们的重组处理程序提供足够的时间来恢复状态。
以下三个部分中的每一个都详细说明了阅读查询和理解计算时需要注意的关键注意事项。 这些注意事项的摘要是:
RewardsCoordinator
中的 SNAPSHOT_CADENCE
earner
:接收奖励的实体,即 staker 或 operatorcalculationIntervalSeconds
:奖励提交持续时间必须是的倍数SNAPSHOT_CADENCE
:拍摄 EigenLayer 核心合约状态快照的频率typo rewardSnaphot -> rewardSnapshot
:快照中给 earner 的奖励cutoff-date
:运行转换的日期。 始终设置为前一天的 UTC 时间 0:00run
:每日奖励管道作业的迭代stakeWeight
:AVS 如何评估其 earner 的 stake,由每个奖励策略的乘数给出gold_table
:包含 rewardSnapshots
的表。 其列为 earner
、amount
、token
、snapshot
、reward_hash
份额转换为 Decimal(78,0),这是一种最多可容纳 uint256 的数据类型。 允许存款的 token(所有 LST 和 Eigen)和原生 ETH 不应出现截断问题。
我们在每次运行时使用以下逻辑设置截止日期:
def get_cutoff_date():
# 获取 UTC 中的当前时间
ts = datetime.now(timezone.utc)
# 向下舍入到当天 UTC 时间 00:00
ts = ts.replace(hour=0, minute=0, second=0, microsecond=0)
# 减去 1 天
ts = ts - timedelta(days=1)
return ts
在管道的每日运行时,如果运行是回填,我们将获取传入的变量。 在回填运行时,我们强制执行开始日期和结束日期有效,即结束日期不得晚于截止日期,并且开始日期不得晚于结束日期。
如果在管道运行中遗漏了事件,则在最坏的情况下运行回填。 我们与多个数据供应商运行协调以确保不必这样做。 此外,我们在管道生成的末尾运行健全性检查查询,以确保:
earner
、reward_hash
和 snapshot
) 的行数永远不会减少def get_gold_calculation_dates(**kwargs):
# 截止日期
cutoff_date = get_cutoff_date()
cutoff_date_str = cutoff_date.strftime('%Y-%m-%d %H:%M:%S')
# 回填日期
dag_run = kwargs.get('dag_run')
if dag_run is not None:
start_date_str = dag_run.conf.get('start_date', '1970-01-01 00:00:00')
end_date_str = dag_run.conf.get('end_date', cutoff_date_str)
is_backfill = str.lower(dag_run.conf.get('is_backfill', 'false'))
else:
raise ValueError('Dag run is None')
# 清理开始和结束日期
start_datetime = datetime.strptime(start_date_str, '%Y-%m-%d %H:%M:%S')
end_datetime = datetime.strptime(end_date_str, '%Y-%m-%d %H:%M:%S')
cutoff_datetime = datetime.strptime(cutoff_date_str, '%Y-%m-%d %H:%M:%S')
if start_datetime >= end_datetime:
raise ValueError('开始日期必须在结束日期之前')
if end_datetime > cutoff_datetime:
raise ValueError('结束日期必须小于或等于截止日期')
# 推送到 XCom
kwargs['ti'].xcom_push(key='cutoff_date', value=end_date_str)
kwargs['ti'].xcom_push(key='rewards_start', value=start_date_str)
kwargs['ti'].xcom_push(key='is_backfill', value=is_backfill)
这组查询从 EigenLayer Core 合约中提取事件数据。 事件日志会自动从合约 ABI 中解码 here。 运行 forge build
将构建合约,ABI 存储在 /out
文件夹中。
在下面的查询中,block_date
是区块的日期,而 block_time
是区块的完整日期 + 时间。
SELECT
lower(coalesce(t.output_data ->> 'depositor', t.output_data ->> 'staker')) as staker,
lower(t.output_data ->> 'strategy') as strategy,
(t.output_data ->> 'shares')::numeric(78,0) as shares,
t.transaction_hash,
t.log_index,
b.block_time,
to_char(b.block_time, 'YYYY-MM-DD') AS block_date,
t.block_number
FROM transaction_logs as t
LEFT JOIN blocks as b ON (t.block_sequence_id = b.id)
WHERE t.address = '{{ var('strategy_manager_address') }}'
AND t.event_name = 'Deposit'
AND date_trunc('day', b.block_time) < TIMESTAMP '{{ var("cutoff_date") }}'
注意:份额可以是负数
SELECT
lower(t.arguments #>> '{0,Value}') AS staker,
(t.output_data ->> 'sharesDelta')::numeric(78,0) as shares,
t.transaction_hash,
t.log_index,
b.block_time,
to_char(b.block_time, 'YYYY-MM-DD') AS block_date,
t.block_number
FROM transaction_logs t
LEFT JOIN blocks b ON t.block_sequence_id = b.id
WHERE t.address = '{{ var('eigen_pod_manager_address') }}'
AND t.event_name = 'PodSharesUpdated'
AND date_trunc('day', b.block_time) < TIMESTAMP '{{ var("cutoff_date") }}'
M1 中的提款通过 StrategyManager 路由。 请注意,我们删除了作为 M1 中的份额完成的单笔提款,因为此代码路径没有存款事件。
SELECT
lower(coalesce(t.output_data ->> 'depositor', t.output_data ->> 'staker')) as staker,
lower(t.output_data ->> 'strategy') as strategy,
(t.output_data ->> 'shares')::numeric(78,0) as shares,
t.transaction_hash,
t.log_index,
b.block_time,
to_char(b.block_time, 'YYYY-MM-DD') AS block_date,
t.block_number
FROM transaction_logs t
LEFT JOIN blocks b ON t.block_sequence_id = b.id
WHERE t.address = '{{ var('strategy_manager_address') }}'
AND t.event_name = 'ShareWithdrawalQueued'
AND date_trunc('day', b.block_time) < TIMESTAMP '{{ var("cutoff_date") }}'
-- 删除此交易哈希,因为它是 m1 上唯一以份额完成的提款。 没有相应的存款事件。 提款已完成到同一 staker 地址。
AND t.transaction_hash != '0x62eb0d0865b2636c74ed146e2d161e39e42b09bac7f86b8905fc7a830935dc1e'
与 M1 提款事件不同,M2 提款事件返回一个元组,其中包含策略和份额的列表。 因此,我们将元组拆分为单独的行以创建(staker,strategy$_0$,share$_0$),(staker,strategy$_1$,share$_1$)。 我们丢弃从 M1 迁移的所有 M2 提款,因此我们不会重复计算提款。
WITH migrations AS (
SELECT
(
SELECT lower(string_agg(lpad(to_hex(elem::int), 2, '0'), ''))
FROM jsonb_array_elements_text(t.output_data->'oldWithdrawalRoot') AS elem
) AS m1_withdrawal_root,
(
SELECT lower(string_agg(lpad(to_hex(elem::int), 2, '0'), ''))
FROM jsonb_array_elements_text(t.output_data->'newWithdrawalRoot') AS elem
) AS m2_withdrawal_root
FROM transaction_logs t
WHERE t.address = '{{ var('delegation_manager_address') }}'
AND t.event_name = 'WithdrawalMigrated'
),
full_m2_withdrawals AS (
SELECT
lower(t.output_data #>> '{withdrawal}') as withdrawals,
(
SELECT lower(string_agg(lpad(to_hex(elem::int), 2, '0'), ''))
FROM jsonb_array_elements_text(t.output_data ->'withdrawalRoot') AS elem
) AS withdrawal_root,
lower(t.output_data #>> '{withdrawal, staker}') AS staker,
lower(t_strategy.strategy) AS strategy,
(t_share.share)::numeric(78,0) AS shares,
t_strategy.strategy_index,
t_share.share_index,
t.transaction_hash,
t.log_index,
b.block_time::timestamp(6),
to_char(b.block_time, 'YYYY-MM-DD') AS block_date,
t.block_number
FROM transaction_logs t
LEFT JOIN blocks b ON t.block_sequence_id = b.id,
jsonb_array_elements_text(t.output_data #> '{withdrawal, strategies}') WITH ORDINALITY AS t_strategy(strategy, strategy_index),
jsonb_array_elements_text(t.output_data #> '{withdrawal, shares}') WITH ORDINALITY AS t_share(share, share_index)
WHERE t.address = '{{ var('delegation_manager_address') }}'
AND t.event_name = 'WithdrawalQueued'
AND date_trunc('day', b.block_time) < TIMESTAMP '{{ var("cutoff_date") }}'
AND t_strategy.strategy_index = t_share.share_index
)
-- 解析出从 m1 迁移的 m2 提款
SELECT
full_m2_withdrawals.*
FROM
full_m2_withdrawals
LEFT JOIN
migrations
ON
full_m2_withdrawals.withdrawal_root = migrations.m2_withdrawal_root
WHERE
migrations.m2_withdrawal_root IS NULL
Operator 状态由 staker 委托给他们的 stake 组成。
SELECT
lower(t.arguments #>> '{0,Value}') as operator,
lower(t.output_data ->> 'strategy') as strategy,
(t.output_data ->> 'shares')::numeric(78,0) as shares,
t.transaction_hash,
t.log_index,
b.block_time,
to_char(b.block_time, 'YYYY-MM-DD') AS block_date,
t.block_number
FROM transaction_logs t
LEFT JOIN blocks b ON t.block_sequence_id = b.id
WHERE t.address = '{{ var('delegation_manager_address') }}'
AND t.event_name = 'OperatorSharesIncreased'
AND date_trunc('day', b.block_time) < TIMESTAMP '{{ var("cutoff_date") }}'
SELECT
lower(t.arguments #>> '{0,Value}') as operator,
lower(t.output_data ->> 'strategy') as strategy,
(t.output_data ->> 'shares')::numeric(78,0) as shares,
t.transaction_hash,
t.log_index,
b.block_time,
to_char(b.block_time, 'YYYY-MM-DD') AS block_date,
t.block_number
FROM transaction_logs t
LEFT JOIN blocks b ON t.block_sequence_id = b.id
WHERE t.address = '{{ var('delegation_manager_address') }}'
AND t.event_name = 'OperatorSharesDecreased'
AND date_trunc('day', b.block_time) < TIMESTAMP '{{ var("cutoff_date") }}'
SELECT
lower(t.arguments #>> '{0,Value}') AS staker,
lower(t.arguments #>> '{1,Value}') AS operator,
t.transaction_hash,
t.log_index,
b.block_time,
to_char(b.block_time, 'YYYY-MM-DD') AS block_date,
t.block_number
FROM transaction_logs t
LEFT JOIN blocks b ON t.block_sequence_id = b.id
WHERE t.address = '{{ var('delegation_manager_address') }}'
AND t.event_name = 'StakerDelegated'
AND date_trunc('day', b.block_time) < TIMESTAMP '{{ var("cutoff_date") }}'
SELECT
lower(t.arguments #>> '{0,Value}') AS staker,
lower(t.arguments #>> '{1,Value}') AS operator,
t.transaction_hash,
t.log_index,
b.block_time,
to_char(b.block_time, 'YYYY-MM-DD') AS block_date,
t.block_number
FROM transaction_logs t
LEFT JOIN blocks b ON t.block_sequence_id = b.id
WHERE t.address = '{{ var('delegation_manager_address') }}'
AND t.event_name = 'StakerUndelegated'
AND date_trunc('day', b.block_time) < TIMESTAMP '{{ var("cutoff_date") }}'
每天跟踪三种类型的 operator 拆分:
拆分计算遵循以下规则:
Operator-AVS 拆分快照示例:
WITH operator_avs_splits_with_block_info as (
SELECT
oas.operator,
oas.avs,
oas.activated_at::timestamp(6) as activated_at,
oas.new_operator_avs_split_bips as split,
oas.block_number,
oas.log_index,
b.block_time::timestamp(6) as block_time
FROM operator_avs_splits as oas
JOIN blocks as b on (b.number = oas.block_number)
WHERE activated_at < TIMESTAMP '{{.cutoffDate}}'
)
协议中有四种类型的奖励提交:
注意:“奖励协调器”(RewardsCoordinator)中的金额最大值为$1e38-1$,这允许我们将其截断为 DECIMAL(38,0)。
对于每个奖励提交,我们在单独的行中提取每个(strategy、multiplier),以便于核算
SELECT
lower(tl.arguments #>> '{0,Value}') AS avs,
lower(tl.arguments #>> '{2,Value}') AS reward_hash,
coalesce(lower(tl.output_data #>> '{rewardsSubmission}'), lower(tl.output_data #>> '{rangePayment}')) as rewards_submission,
coalesce(lower(tl.output_data #>> '{rewardsSubmission, token}'), lower(tl.output_data #>> '{rangePayment, token}')) as token,
coalesce(tl.output_data #>> '{rewardsSubmission,amount}', tl.output_data #>> '{rangePayment,amount}')::numeric(78,0) as amount,
to_timestamp(coalesce(tl.output_data #>> '{rewardsSubmission,startTimestamp}', tl.output_data #>> '{rangePayment,startTimestamp}')::bigint)::timestamp(6) as start_timestamp,
coalesce(tl.output_data #>> '{rewardsSubmission,duration}', tl.output_data #>> '{rangePayment,duration}')::bigint as duration,
to_timestamp(
coalesce(tl.output_data #>> '{rewardsSubmission,startTimestamp}', tl.output_data #>> '{rangePayment,startTimestamp}')::bigint
+ coalesce(tl.output_data #>> '{rewardsSubmission,duration}', tl.output_data #>> '{rangePayment,duration}')::bigint
)::timestamp(6) as end_timestamp,
lower(t.entry ->> 'strategy') as strategy,
(t.entry ->> 'multiplier')::numeric(78,0) as multiplier,
t.strategy_index as strategy_index,
tl.transaction_hash,
tl.log_index,
b.block_time::timestamp(6),
to_char(b.block_time, 'YYYY-MM-DD') AS block_date,
tl.block_number
FROM transaction_logs tl
LEFT JOIN blocks b ON (tl.block_sequence_id = b.id)
CROSS JOIN LATERAL jsonb_array_elements(
coalesce(tl.output_data #> '{rewardsSubmission,strategiesAndMultipliers}',tl.output_data #> '{rangePayment,strategiesAndMultipliers}')
) WITH ORDINALITY AS t(entry, strategy_index)
WHERE address = '{{ var('rewards_coordinator_address') }}'
AND event_name = 'AVSRewardsSubmissionCreated'
AND date_trunc('day', b.block_time) < TIMESTAMP '{{ var("cutoff_date") }}'
SELECT
lower(tl.arguments #>> '{0,Value}') AS avs, -- 保留为 AVS,以便与 range_payments 上的联合兼容。
lower(tl.arguments #>> '{2,Value}') AS reward_hash,
coalesce(lower(tl.output_data #>> '{rewardsSubmission}'), lower(tl.output_data #>> '{rangePayment}')) as rewards_submission,
coalesce(lower(tl.output_data #>> '{rewardsSubmission, token}'), lower(tl.output_data #>> '{rangePayment, token}')) as token,
coalesce(tl.output_data #>> '{rewardsSubmission,amount}', tl.output_data #>> '{rangePayment,amount}')::numeric(78,0) as amount,
to_timestamp(coalesce(tl.output_data #>> '{rewardsSubmission,startTimestamp}', tl.output_data #>> '{rangePayment,startTimestamp}')::bigint)::timestamp(6) as start_timestamp,
coalesce(tl.output_data #>> '{rewardsSubmission,duration}', tl.output_data #>> '{rangePayment,duration}')::bigint as duration,
to_timestamp(
coalesce(tl.output_data #>> '{rewardsSubmission,startTimestamp}', tl.output_data #>> '{rangePayment,startTimestamp}')::bigint
+ coalesce(tl.output_data #>> '{rewardsSubmission,duration}', tl.output_data #>> '{rangePayment,duration}')::bigint
)::timestamp(6) as end_timestamp,
lower(t.entry ->> 'strategy') as strategy,
(t.entry ->> 'multiplier')::numeric(78,0) as multiplier,
t.strategy_index as strategy_index,
tl.transaction_hash,
tl.log_index,
b.block_time::timestamp(6),
to_char(b.block_time, 'YYYY-MM-DD') AS block_date,
tl.block_number
FROM transaction_logs tl
LEFT JOIN blocks b ON tl.block_sequence_id = b.id
CROSS JOIN LATERAL jsonb_array_elements(
coalesce(tl.output_data #> '{rewardsSubmission,strategiesAndMultipliers}',tl.output_data #> '{rangePayment,strategiesAndMultipliers}')
) WITH ORDINALITY AS t(entry, strategy_index)
WHERE address = '{{ var('rewards_coordinator_address') }}'
AND event_name = 'RewardsSubmissionForAllCreated'
AND date_trunc('day', b.block_time) < TIMESTAMP '{{ var("cutoff_date") }}'
SELECT
lower(tl.arguments #>> '{0,Value}') AS avs, -- 保留为 AVS,以便与 range_payments 上的联合兼容。
lower(tl.arguments #>> '{2,Value}') AS reward_hash,
coalesce(lower(tl.output_data #>> '{rewardsSubmission}'), lower(tl.output_data #>> '{rangePayment}')) as rewards_submission,
coalesce(lower(tl.output_data #>> '{rewardsSubmission, token}'), lower(tl.output_data #>> '{rangePayment, token}')) as token,
coalesce(tl.output_data #>> '{rewardsSubmission,amount}', tl.output_data #>> '{rangePayment,amount}')::numeric(78,0) as amount,
to_timestamp(coalesce(tl.output_data #>> '{rewardsSubmission,startTimestamp}', tl.output_data #>> '{rangePayment,startTimestamp}')::bigint)::timestamp(6) as start_timestamp,
coalesce(tl.output_data #>> '{rewardsSubmission,duration}', tl.output_data #>> '{rangePayment,duration}')::bigint as duration,
to_timestamp(
coalesce(tl.output_data #>> '{rewardsSubmission,startTimestamp}', tl.output_data #>> '{rangePayment,startTimestamp}')::bigint
+ coalesce(tl.output_data #>> '{rewardsSubmission,duration}', tl.output_data #>> '{rangePayment,duration}')::bigint
)::timestamp(6) as end_timestamp,
lower(t.entry ->> 'strategy') as strategy,
(t.entry ->> 'multiplier')::numeric(78,0) as multiplier,
t.strategy_index as strategy_index,
tl.transaction_hash,
tl.log_index,
b.block_time::timestamp(6),
to_char(b.block_time, 'YYYY-MM-DD') AS block_date,
tl.block_number
FROM transaction_logs tl
LEFT JOIN blocks b ON tl.block_sequence_id = b.id
CROSS JOIN LATERAL jsonb_array_elements(
coalesce(tl.output_data #> '{rewardsSubmission,strategiesAndMultipliers}',tl.output_data #> '{rangePayment,strategiesAndMultipliers}')
) WITH ORDINALITY AS t(entry, strategy_index)
WHERE address = '{{ var('rewards_coordinator_address') }}'
AND event_name = 'RewardsSubmissionForAllEarnersCreated'
AND date_trunc('day', b.block_time) < TIMESTAMP '{{ var("cutoff_date") }}'
WITH _operator_directed_rewards as (
SELECT
odrs.avs,
odrs.reward_hash,
odrs.token,
odrs.operator,
odrs.operator_index,
odrs.amount,
odrs.strategy,
odrs.strategy_index,
odrs.multiplier,
odrs.start_timestamp::TIMESTAMP(6),
odrs.end_timestamp::TIMESTAMP(6),
odrs.duration,
odrs.block_number,
b.block_time::TIMESTAMP(6),
TO_CHAR(b.block_time, 'YYYY-MM-DD') AS block_date
FROM operator_directed_reward_submissions AS odrs
JOIN blocks AS b ON(b.number = odrs.block_number)
WHERE b.block_time < TIMESTAMP '{{.cutoffDate}}'
)
SELECT
avs,
reward_hash,
token,
operator,
operator_index,
amount,
strategy,
strategy_index,
multiplier,
start_timestamp::TIMESTAMP(6),
end_timestamp::TIMESTAMP(6),
duration,
block_number,
block_time,
block_date
FROM _operator_directed_rewards
SELECT
odosrs.avs,
odosrs.operator_set_id,
odosrs.reward_hash,
odosrs.token,
odosrs.operator,
odosrs.operator_index,
odosrs.amount,
odosrs.strategy,
odosrs.strategy_index,
odosrs.multiplier,
odosrs.start_timestamp::TIMESTAMP(6),
odosrs.end_timestamp::TIMESTAMP(6),
odosrs.duration,
odosrs.block_number,
b.block_time::TIMESTAMP(6),
TO_CHAR(b.block_time, 'YYYY-MM-DD') AS block_date
FROM operator_directed_operator_set_reward_submissions AS odosrs
JOIN blocks AS b ON (b.number = odosrs.block_number)
WHERE b.block_time < TIMESTAMP '{{.cutoffDate}}'
从 operator 到 AVS 的每次注销和注册都记录在 AVSDirectory 中
SELECT
lower(t.arguments #>> '{0,Value}') as operator,
lower(t.arguments #>> '{1,Value}') as avs,
(t.output_data -> 'status')::int as status,
t.transaction_hash,
t.log_index,
b.block_time,
to_char(b.block_time, 'YYYY-MM-DD') AS block_date,
t.block_number
FROM transaction_logs t
LEFT JOIN blocks b ON t.block_sequence_id = b.id
WHERE t.address = '{{ var('avs_directory_address') }}'
AND t.event_name = 'OperatorAVSRegistrationStatusUpdated'
AND date_trunc('day', b.block_time) < TIMESTAMP '{{ var("cutoff_date") }}'
AVS Directory 不为 operator 在 AVS 上重新质押或取消重新质押的策略发出事件。 为了检索此信息,我们每 3600 个区块(即,blockNum % 3600 = 0
)运行一个 cron 作业,从部署 AVSDirectory 时开始,该作业执行以下操作:
getOperatorRestakedStrategies(address operator) returns (address[])
要求 AVS 符合此接口,如我们的 docs 中所述
假设 operator 在时间戳 $t$ 注册到 AVS,则此 cron 作业的示例输出为:
Operator | AVS | 策略 | 区块时间 |
---|---|---|---|
Operator1 | AVS-A | stETH | t |
Operator1 | AVS-A | rETH | t |
Operator2 | AVS-A | rETH | t |
Operator3 | AVS-B | cbETH | t |
每个 operator{'<>'}operator set 注册和注销都记录在 AllocationManager 合约中。
select
osor.*,
b.block_time::timestamp(6) as block_time,
to_char(b.block_time, 'YYYY-MM-DD') AS block_date
from operator_set_operator_registrations as osor
join blocks as b on (b.number = osor.block_number)
where b.block_time < TIMESTAMP '{{.cutoffDate}}'
select
ossr.*,
b.block_time::timestamp(6) as block_time,
to_char(b.block_time, 'YYYY-MM-DD') AS block_date
from operator_set_strategy_registrations as ossr
join blocks as b on (b.number = ossr.block_number)
where b.block_time < TIMESTAMP '{{.cutoffDate}}'
一旦我们提取了 EigenLayer 核心合约和 AVS 的所有日志和相关存储,我们就对其进行转换以创建状态的每日快照,分为两部分
在第 2 部分中,一旦状态被聚合,我们就将状态范围展开为每日快照。
状态快照向上舍入到最接近的一天,但 operator{'<>'}avs 注销除外,这些注销向下舍入。 让我们假设我们有以下范围,其中事件 A 和 B 是 staker 份额的更新。
GENESIS_TIMESTAMP---------------------Day1---------------------Day2
^ ^
A=100 B=200
快照转换的输出应表示在 Day1 上,Staker 拥有 200 个份额。 更一般地,我们采用 [Day$_{i-1}$, Day$_i$] 范围内的最新更新,并将其设置为 Day$_i$ 上的状态。 我们将给定一天的奖励称为奖励快照。
在 operator 注册和注销的情况下:
GENESIS_TIMESTAMP---------------------Day1---------------------Day2
^ ^
注册 注销
最终状态是,operator 已在第 1 天注册和注销,导致没有向 operator 发放奖励。 我们添加此机制是为了保护 operator,如果我们要向上舍入注销,他们将获得额外的奖励天数。 副作用如下:
--------------Day1--------------Day2--------------Day3--------------Day4
^ ^
注册 注销
```在这种情况下,operator 将在 Day3 注销,导致 operator 在 [Day3,Day4] 范围内没有收到任何奖励,因为它正在保护 AVS。向下取整注销日期是为什么 `cutoff_date` 必须是 **前一天** 的 0:00 UTC。
#### Operator{'<>'}Operator 集注册/注销
在 operator{'<>'}operator 集注册和注销的情况下:
GENESIS_TIMESTAMP---------------------Day1---------------------Day2 ^ ^ 注册 注销
最终状态是 operator 在第 1 天注册和注销,导致没有向 operator 发放任何奖励。我们添加这个机制是为了防止如果我们向上取整注销日期,operator 获得额外的奖励天数。副作用如下:
--------------Day1--------------Day2--------------Day3--------------Day4 ^ ^ 注册 注销
在这种情况下,operator 将在 Day3 注销,导致 operator 在 [Day3,Day4] 范围内没有收到任何奖励,因为它正在保护 operator 集。向下取整注销日期是为什么 `cutoff_date` 必须是 **前一天** 的 0:00 UTC。
## 第一部分:聚合
### Staker 份额
staker $s$ 和策略 $y$ 的 LST 份额由下式给出:
Shares$_{s,y}$ = Deposits$_{s,y}$ $-$ M1Withdrawals$_{s,y}$ $-$ M2Withdrawals$_{s,y}$
staker 的 Native ETH 份额是给定 staker 的所有 `PodSharesUpdated` 事件的总和。请注意,在此事件中,份额**_可能为负_**。
NativeETHShares$_s$ = $\sum_{i=0}^{n}$ PodSharesUpdated$_i$- M1Withdrawals$_i$ - M2Withdrawals$_i$
将这两个结合起来,我们就得到了每个更新的每个策略的 staker 份额。
此查询的关键部分是:
```sql
SUM(shares) OVER (PARTITION BY staker, strategy ORDER BY block_time, log_index) AS shares,
它获取每个更新处每个(staker,strategy)对的运行总和。
SELECT
staker,
strategy,
-- 对窗口中的每个份额金额求和,以在每次时间戳更新时获得每个 (staker, strategy) 的总份额 */
SUM(shares) OVER (PARTITION BY staker, strategy ORDER BY block_time, log_index) AS shares,
transaction_hash,
log_index,
strategy_index,
block_time,
block_date,
block_number
FROM (
SELECT staker, strategy, shares, 0 as strategy_index, transaction_hash, log_index, block_time, block_date, block_number
FROM {{ ref('staker_deposits') }}
UNION ALL
-- 减去 m1 & m2 提款
SELECT staker, strategy, shares * -1, 0 as strategy_index, transaction_hash, log_index, block_time, block_date, block_number
FROM {{ ref('m1_staker_withdrawals') }}
UNION ALL
SELECT staker, strategy, shares * -1, strategy_index, transaction_hash, log_index, block_time, block_date, block_number
FROM {{ ref('m2_staker_withdrawals') }}
UNION all
-- eigenpod 中的份额是正数或负数,因此无需乘以 -1
SELECT staker, '0xbeac0eeeeeeeeeeeeeeeeeeeeeeeeeeeeeebeac0' as strategy, shares, 0 as strategy_index, transaction_hash, log_index, block_time, block_date, block_number
FROM {{ ref('eigenpod_shares') }}
) combined_staker_shares
注意:所有奖励都不会支付给那些尚未向执行层证明其信标链余额的 staker。
operator $o$ 对于策略 $y$ 的份额由以下公式给出: $Shares{o,y} = ShareIncrease{o,y} - ShareDecrease_{o,y}$
SELECT
operator,
strategy,
-- 对窗口中的每个份额金额求和,以在每次时间戳更新时获得每个 (operator, strategy) 的总份额 */
SUM(shares) OVER (PARTITION BY operator, strategy ORDER BY block_time, log_index) AS shares,
transaction_hash,
log_index,
block_time,
block_date,
block_number
FROM (
SELECT operator, strategy, shares, transaction_hash, log_index, block_time, block_date, block_number
FROM {{ ref('operator_share_increases') }}
UNION ALL
SELECT operator, strategy, shares * -1 AS shares, transaction_hash, log_index, block_time, block_date, block_number
FROM {{ ref('operator_share_decreases') }}
) combined_shares
在这里,我们将每次委托和取消委托聚合到一个视图中。当 staker 被取消委托时,我们将其 operator 标记为 0x0000000000000000000000000000000000000000
。
SELECT
staker,
CASE when src = 'undelegations' THEN '0x0000000000000000000000000000000000000000' ELSE operator END AS operator,
transaction_hash,
log_index,
block_time,
block_date,
block_number
FROM (
SELECT *, 'undelegations' AS src FROM {{ ref('staker_undelegations') }}
UNION ALL
SELECT *, 'delegations' AS src FROM {{ ref('staker_delegations') }}
) as delegations_combined
将 AVS 奖励提交和所有奖励提交组合到一个视图中,并添加了 reward_type
参数。
SELECT *, 'avs' as reward_type from {{ ref('avs_reward_submissions') }}
UNION ALL
SELECT *, 'all_stakers' as reward_type from {{ ref('reward_submission_for_all') }}
UNION ALL
SELECT *, 'all_earners' as reward_type from {{ ref('reward_submission_for_all_earners') }}
将 AVSDirectory 中的 状态元组 格式化为 true
或 false
。
SELECT
operator,
avs,
CASE WHEN status = 1 then true ELSE false END AS registered,
transaction_hash,
log_index,
block_date,
block_time,
block_number
FROM {{ ref('operator_avs_registrations') }}
一旦我们转换了链上状态,我们就会将状态聚合到状态处于活动状态的一段时间内。最后,状态窗口被展开为每日快照。
我们已经在 上面的考虑因素 中解释过的关键设计决策是,状态总是向上舍入到最近的一天 0:00 UTC,除了 operator{'<>'}avs 取消注册。
WITH ranked_staker_records as (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY staker, strategy, cast(block_time AS DATE) ORDER BY block_time DESC, log_index DESC) AS rn
FROM {{ ref('staker_shares') }}
),
snapshot_time
每天的最新记录 & 向上取整到快照日 snapshotted_records as (
SELECT
staker,
strategy,
shares,
block_time,
date_trunc('day', block_time) + INTERVAL '1' day AS snapshot_time
from ranked_staker_records
where rn = 1
),
staker_share_windows as (
SELECT
staker, strategy, shares, snapshot_time as start_time,
CASE
-- 如果范围没有结束时间,则使用截断为 0 UTC 的当前时间戳
WHEN LEAD(snapshot_time) OVER (PARTITION BY staker, strategy ORDER BY snapshot_time) is null THEN date_trunc('day', TIMESTAMP '{{ var("cutoff_date") }}')
ELSE LEAD(snapshot_time) OVER (PARTITION BY staker, strategy ORDER BY snapshot_time)
END AS end_time
FROM snapshotted_records
)
SELECT * from staker_share_windows
我们在 staker_share_windows
CTE 中使用了 LEAD
运算符。此运算符查找给定(staker
,strategy
)组合的下一个记录。逻辑是:
end_time
设置为 cutoff_date
end_time
设置为下一个记录的 start_time
注意:上述逻辑可能具有 (staker
, strategy
) 组合,其中一个记录的 end_record
等于下一个记录的 start_time
。当我们把窗口展开到快照中时,会对此进行处理。
WITH ranked_operator_records as (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY operator, strategy, cast(block_time AS DATE) ORDER BY block_time DESC, log_index DESC) AS rn
FROM {{ ref('operator_shares') }}
),
-- 获取每天的最新记录 & 向上取整到快照日
snapshotted_records as (
SELECT
operator,
strategy,
shares,
block_time,
date_trunc('day', block_time) + INTERVAL '1' day as snapshot_time
from ranked_operator_records
where rn = 1
),
-- 获取每个 operator,strategy 配对的范围
operator_share_windows as (
SELECT
operator, strategy, shares, snapshot_time as start_time,
CASE
-- 如果范围没有结束时间,则使用截断为 0 UTC 的当前时间戳
WHEN LEAD(snapshot_time) OVER (PARTITION BY operator, strategy ORDER BY snapshot_time) is null THEN date_trunc('day', TIMESTAMP '{{ var("cutoff_date") }}')
ELSE LEAD(snapshot_time) OVER (PARTITION BY operator, strategy ORDER BY snapshot_time)
END AS end_time
FROM snapshotted_records
)
SELECT * from operator_share_windows
此逻辑与 Staker 份额窗口完全相同。
with ranked_delegations as (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY staker, cast(block_time AS DATE) ORDER BY block_time DESC, log_index DESC) AS rn
FROM {{ ref('staker_delegation_status') }}
),
-- 获取每天的最新记录 & 向上取整到快照日
snapshotted_records as (
SELECT
staker,
operator,
block_time,
date_trunc('day', block_time) + INTERVAL '1' day AS snapshot_time
from ranked_delegations
where rn = 1
),
-- 获取每个 staker 的范围
staker_delegation_windows as (
SELECT
staker, operator, snapshot_time as start_time,
CASE
-- 如果范围没有结束时间,则使用截断为 0 UTC 的截止日期
WHEN LEAD(snapshot_time) OVER (PARTITION BY staker ORDER BY snapshot_time) is null THEN date_trunc('day', TIMESTAMP '{{ var("cutoff_date") }}')
ELSE LEAD(snapshot_time) OVER (PARTITION BY staker ORDER BY snapshot_time)
END AS end_time
FROM snapshotted_records
)
SELECT * from staker_delegation_windows
此逻辑与 Staker 份额窗口完全相同。
此计算与上述 3 个查询不同,因为注册窗口不能互相连续。例如,如果 operator 具有以下状态:
Operator | AVS | 已注册 | 日期 |
---|---|---|---|
Operator1 | AVS1 | 真 | 2024-4-24 |
Operator1 | AVS1 | 假 | 2024-4-26 |
Operator1 | AVS1 | 真 | 2024-4-29 |
Operator1 | AVS1 | 假 | 2024-5-1 |
Operator1 | AVS1 | 真 | 2024-5-1 |
Operator1 | AVS1 | 假 | 2024-5-1 |
我们不关心第 2 行和第 3 行的(取消注册,注册)窗口。我们还需要丢弃第 5 行和第 6 行的(注册,取消注册窗口)。
WITH marked_statuses AS (
SELECT
operator,
avs,
registered,
block_time,
block_date,
-- 将下一个操作标记为 next_block_time
LEAD(block_time) OVER (PARTITION BY operator, avs ORDER BY block_time ASC, log_index ASC) AS next_block_time,
-- 下面的 lead/lag 组合仅在下一个 CTE 中使用
-- 获取下一行的注册状态和 block_date
LEAD(registered) OVER (PARTITION BY operator, avs ORDER BY block_time ASC, log_index ASC) AS next_registration_status,
LEAD(block_date) OVER (PARTITION BY operator, avs ORDER BY block_time ASC, log_index ASC) AS next_block_date,
-- 获取上一行的注册状态和 block_date
LAG(registered) OVER (PARTITION BY operator, avs ORDER BY block_time ASC, log_index ASC) AS prev_registered,
LAG(block_date) OVER (PARTITION BY operator, avs ORDER BY block_time ASC, log_index ASC) AS prev_block_date
FROM {{ ref('operator_avs_status') }}
),
removed_same_day_deregistrations AS (
SELECT * from marked_statuses
WHERE NOT (
-- 删除注册部分
(registered = TRUE AND
COALESCE(next_registration_status = FALSE, false) AND -- 如果为空,则默认为 false
COALESCE(block_date = next_block_date, false)) OR
-- 删除取消注册部分
(registered = FALSE AND
COALESCE(prev_registered = TRUE, false) and
COALESCE(block_date = prev_block_date, false)
)
)
),
end_time
标记为下一个记录。如果不是这种情况,则将 end_time
标记为 cutoff_date
registration_periods AS (
SELECT
operator,
avs,
block_time AS start_time,
-- 将 next_block_time 标记为范围的 end_time
-- 使用 coalesce,因为如果注册的 next_block_time 未关闭,那么我们使用 cutoff_date
COALESCE(next_block_time, TIMESTAMP '{{ var("cutoff_date") }}') AS end_time,
registered
FROM removed_same_day_deregistrations
WHERE registered = TRUE
),
start_time
并向下取整每个 end_time registration_windows_extra as (
SELECT
operator,
avs,
date_trunc('day', start_time) + interval '1' day as start_time,
-- 结束时间是排他的结束时间,因为 operator 未在结束时间在 AVS 上注册,或者它是向上取整的当前时间戳
date_trunc('day', end_time) as end_time
FROM registration_periods
),
start_time
和 end_time
的记录: operator_avs_registration_windows as (
SELECT * from registration_windows_extra
WHERE start_time != end_time
)
select * from operator_avs_registration_windows
此查询将来自 operator 重新抵押策略 cron 作业的条目聚合到窗口中。
block_time
向上取整为 0 UTC。Log_index 无关紧要,因为此数据是从 RPC 调用生成的。with ranked_records AS (
SELECT
lower(operator) as operator,
lower(avs) as avs,
lower(strategy) as strategy,
block_time,
date_trunc('day', CAST(block_time as timestamp(6))) + interval '1' day as start_time,
ROW_NUMBER() OVER (
PARTITION BY operator, avs, strategy, date_trunc('day', CAST(block_time as timestamp(6))) + interval '1' day
ORDER BY block_time DESC -- 希望最新的记录排名最高
) AS rn
-- 无法在此处使用 ref,因为此表不是通过 DBT 生成的
FROM public.operator_restaked_strategies
-- testnet 和 holesky 都一起存在于 blocklake 中,因此 avs_directory_address 允许我们过滤
WHERE avs_directory_address = lower('{{ var('avs_directory_address') }}')
),
operator
, avs
, strategy
, day
) 组合的最新记录 latest_records AS (
SELECT
operator,
avs,
strategy,
start_time,
block_time
FROM ranked_records
WHERE rn = 1
),
operator
, avs
, strategy
) 分组的下一个条目。 grouped_records AS (
SELECT
operator,
avs,
strategy,
start_time,
LEAD(start_time) OVER (
PARTITION BY operator, avs, strategy
ORDER BY start_time ASC
) AS next_start_time
FROM latest_records
),
next_start_times
不是正好比当前记录的 start_time
晚一天)。这是因为 operator 将从 AVS 取消注册。 parsed_ranges AS (
SELECT
operator,
avs,
strategy,
start_time,
-- 如果 next_start_time 不在第二天,则关闭 end_time
CASE
WHEN next_start_time IS NULL OR next_start_time > start_time + INTERVAL '1' DAY THEN start_time
ELSE next_start_time
END AS end_time
FROM grouped_records
),
start_time
== end_time
的记录 active_windows as (
SELECT *
FROM parsed_ranges
WHERE start_time != end_time
),
operator
, avs
, strategy
) 组合的连续组。首先,我们标记每行的 prev_end_time
。如果有新窗口,则 gap 为空 gaps_and_islands AS (
SELECT
operator,
avs,
strategy,
start_time,
end_time,
LAG(end_time) OVER(PARTITION BY operator, avs, strategy ORDER BY start_time) as prev_end_time
FROM active_windows
),
prev_end_time
与 start_time
相同,则记录是同一连续分组的一部分。 island_detection AS (
SELECT operator, avs, strategy, start_time, end_time, prev_end_time,
CASE
-- 如果之前的结束时间等于开始时间,则标记为 island 的一部分,否则创建新的 island
WHEN prev_end_time = start_time THEN 0
ELSE 1
END as new_island
FROM gaps_and_islands
),
new_island
创建组。具有相同 island_id
总和的行是同一分组的一部分 island_groups AS (
SELECT
operator,
avs,
strategy,
start_time,
end_time,
SUM(new_island) OVER (
PARTITION BY operator, avs, strategy ORDER BY start_time
) AS island_id
FROM island_detection
),
operator_avs_strategy_windows AS (
SELECT
operator,
avs,
strategy,
MIN(start_time) AS start_time,
MAX(end_time) AS end_time
FROM island_groups
GROUP BY operator, avs, strategy, island_id
ORDER BY operator, avs, strategy, start_time
)
select * from operator_avs_strategy_windows
在此计算中,注册窗口不能互相连续。例如,如果 operator 具有以下状态:
Operator | Operator 集 | 已注册 | 日期 |
---|---|---|---|
Operator1 | Operator 集 1 | True | 2024-4-24 |
Operator1 | Operator 集 1 | 假 | 2024-4-26 |
Operator1 | Operator 集 1 | True | 2024-4-29 |
Operator1 | Operator 集 1 | 假 | 2024-5-1 |
Operator1 | Operator 集 1 | True | 2024-5-1 |
Operator1 | Operator 集 1 | 假 | 2024-5-1 |
我们不关心第 2 行和第 3 行的(取消注册,注册)窗口。我们还需要丢弃第 5 行和第 6 行的(注册,取消注册窗口)。
此外,operator 取消注册向下舍入,注册向上舍入。
marked_statuses AS (
SELECT
operator,
avs,
operator_set_id,
is_active,
block_time,
block_date,
-- 将下一个操作标记为 next_block_time
LEAD(block_time) OVER (PARTITION BY operator, avs, operator_set_id ORDER BY block_time ASC, log_index ASC) AS next_block_time,
-- 下面的 lead/lag 组合仅在下一个 CTE 中使用
-- 获取下一行的注册状态和 block_date
LEAD(is_active) OVER (PARTITION BY operator, avs, operator_set_id ORDER BY block_time ASC, log_index ASC) AS next_is_active,
LEAD(block_date) OVER (PARTITION BY operator, avs, operator_set_id ORDER BY block_time ASC, log_index ASC) AS next_block_date,
-- 获取上一行的注册状态和 block_date
LAG(is_active) OVER (PARTITION BY operator, avs, operator_set_id ORDER BY block_time ASC, log_index ASC) AS prev_is_active,
LAG(block_date) OVER (PARTITION BY operator, avs, operator_set_id ORDER BY block_time ASC, log_index ASC) AS prev_block_date
FROM state_changes
),
removed_same_day_deregistrations AS (
SELECT * from marked_statuses
WHERE NOT (
-- 删除注册部分
(is_active = TRUE AND
COALESCE(next_is_active = FALSE, false) AND -- 如果为空,则默认为 false
COALESCE(block_date = next_block_date, false)) OR
-- 删除取消注册部分
(is_active = FALSE AND
COALESCE(prev_is_active = TRUE, false) and
COALESCE(block_date = prev_block_date, false)
)
)
),
end_time
标记为下一个记录。如果不是这种情况,则将 end_time
标记为 cutoff_date
registration_periods AS (
SELECT
operator,
avs,
operator_set_id,
block_time AS start_time,
-- 将 next_block_time 标记为范围的 end_time
-- 使用 coalesce,因为如果注册的 next_block_time 未关闭,那么我们使用 cutoff_date
COALESCE(next_block_time, '{{.cutoffDate}}')::timestamp AS end_time,
is_active
FROM removed_same_day_deregistrations
WHERE is_active = TRUE
),
start_time
并向下取整每个 end_timeregistration_windows_extra as (
SELECT
operator,
avs,
operator_set_id,
date_trunc('day', start_time) + interval '1' day as start_time,
-- 结束时间是排他的结束时间,因为 strategy 未在 operator 集的结束时间注册,或者它是向上取整的当前时间戳
date_trunc('day', end_time) as end_time
FROM registration_periods
),
start_time
和 end_time
的记录:operator_set_operator_registration_windows as (
SELECT * from registration_windows_extra
WHERE start_time != end_time
),
在此计算中,注册窗口不能互相连续。例如,如果 strategy 具有以下状态:
Strategy | Operator 集 | 已注册 | 日期 |
---|---|---|---|
Strategy1 | Operator 集 1 | True | 2024-4-24 |
Strategy1 | Operator 集 1 | 假 | 2024-4-26 |
Strategy1 | Operator 集 1 | True | 2024-4-29 |
Strategy1 | Operator 集 1 | 假 | 2024-5-1 |
Strategy1 | Operator 集 1 | True | 2024-5-1 |
Strategy1 | Operator 集 1 | 假 | 2024-5-1 |
我们不关心第 2 行和第 3 行的(取消注册,注册)窗口。我们还需要丢弃第 5 行和第 6 行的(注册,取消注册窗口)。
此外,strategy 注册和取消注册都向上取整。
marked_statuses AS (
SELECT
strategy,
avs,
operator_set_id,
is_active,
block_time,
block_date,
-- 将下一个操作标记为 next_block_time
LEAD(block_time) OVER (PARTITION BY strategy, avs, operator_set_id ORDER BY block_time ASC, log_index ASC) AS next_block_time,
-- 下面的 lead/lag 组合仅在下一个 CTE 中使用
-- 获取下一行的注册状态和 block_date
LEAD(is_active) OVER (PARTITION BY strategy, avs, operator_set_id ORDER BY block_time ASC, log_index ASC) AS next_is_active,
LEAD(block_date) OVER (PARTITION BY strategy, avs, operator_set_id ORDER BY block_time ASC, log_index ASC) AS next_block_date,
-- 获取上一行的注册状态和 block_date
LAG(is_active) OVER (PARTITION BY strategy, avs, operator_set_id ORDER BY block_time ASC, log_index ASC) AS prev_is_active,
LAG(block_date) OVER (PARTITION BY strategy, avs, operator_set_id ORDER BY block_time ASC, log_index ASC) AS prev_block_date
FROM state_changes
),
removed_same_day_deregistrations AS (
SELECT * from marked_statuses
WHERE NOT (
-- 删除注册部分
(is_active = TRUE AND
COALESCE(next_is_active = FALSE, false) AND -- 如果为空,则默认为 false
COALESCE(block_date = next_block_date, false)) OR
-- 删除取消注册部分
(is_active = FALSE AND
COALESCE(prev_is_active = TRUE, false) and
COALESCE(block_date = prev_block_date, false)
)
)
),
end_time
标记为下一个记录。如果不是这种情况,则将 end_time
标记为 cutoff_date
registration_periods AS (
SELECT
strategy,
avs,
operator_set_id,
block_time AS start_time,
-- 将 next_block_time 标记为范围的 end_time
-- 使用 coalesce,因为如果注册的 next_block_time 未关闭,那么我们使用 cutoff_date
COALESCE(next_block_time, '{{.cutoffDate}}')::timestamp AS end_time,
is_active
FROM removed_same_day_deregistrations
WHERE is_active = TRUE
),
start_time
和 end_time
registration_windows_extra as (
SELECT
strategy,
avs,
operator_set_id,
date_trunc('day', start_time) + interval '1' day as start_time,
-- 结束时间向上舍入以包括注册的完整最后一天
date_trunc('day', end_time) + interval '1' day as end_time
FROM registration_periods
),
start_time
和 end_time
的记录:operator_set_strategy_registration_windows as (
SELECT * from registration_windows_extra
WHERE start_time != end_time
),
一旦我们为核心合同状态的每个表创建了窗口,我们将这些窗口展开为每日快照。在下面的每个查询中,我们将 end_time
向下舍入一天,因为新记录可以在同一天开始,或者它将被包含在 cutoff_date
之后的单独流水线运行中。
WITH cleaned_records as (
SELECT * FROM {{ ref('staker_share_windows')}}
WHERE start_time < end_time
)
SELECT
staker,
strategy,
shares,
cast(day AS DATE) AS snapshot
FROM
cleaned_records
CROSS JOIN
generate_series(DATE(start_time), DATE(end_time) - interval '1' day, interval '1' day) AS day
我们删除任何具有错误 start_time
和 end_time
值的记录。然后,我们展开整个范围。
WITH cleaned_records as (
SELECT * FROM {{ ref('operator_share_windows')}}
WHERE start_time < end_time
)
SELECT
operator,
strategy,
shares,
cast(day AS DATE) AS snapshot
FROM
cleaned_records
CROSS JOIN
generate_series(DATE(start_time), DATE(end_time) - interval '1' day, interval '1' day) AS day
WITH cleaned_records as (
SELECT * FROM {{ ref('staker_delegation_windows') }}
WHERE start_time < end_time
)
SELECT
staker,
operator,
cast(day AS DATE) AS snapshot
FROM
cleaned_records
CROSS JOIN
generate_series(DATE(start_time), DATE(end_time) - interval '1' day, interval '1' day) AS day
WITH cleaned_records AS (
SELECT * FROM {{ ref('operator_avs_strategy_windows') }}
WHERE start_time < end_time
)
SELECT
operator,
avs,
strategy,
cast(day AS DATE) AS snapshot
FROM
cleaned_records
CROSS JOIN
generate_series(DATE(start_time), DATE(end_time) - interval '1' day, interval '1' day) AS day
WITH cleaned_records AS (
SELECT * FROM {{ ref('operator_avs_registration_windows') }}
WHERE start_time < end_time
)
SELECT
operator,
avs,
day AS snapshot
FROM cleaned_records
CROSS JOIN generate_series(DATE(start_time), DATE(end_time) - interval '1' day, interval '1' day) AS day
WITH cleaned_records AS (
SELECT * FROM operator_set_operator_registration_windows
WHERE start_time < end_time
)
SELECT
operator,
avs,
operator_set_id,
d AS snapshot
FROM cleaned_records
CROSS JOIN generate_series(DATE(start_time), DATE(end_time) - interval '1' day, interval '1' day) AS d
WITH cleaned_records AS (
SELECT * FROM operator_set_strategy_registration_windows
WHERE start_time < end_time
)
SELECT
strategy,
avs,
operator_set_id,
d AS snapshot
FROM cleaned_records
CROSS JOIN generate_series(DATE(start_time), DATE(end_time) - interval '1' day, interval '1' day) AS d
奖励分配是从每日状态快照计算得出的。例如,如果我们有一个以下范围的奖励提交:
Day0------Day1------Day2------Day3------Day4------Day5------Day6------Day7
奖励管道将从 7 个状态快照计算奖励分配:Day1、Day2、...、Day7。我们包括最后一天(Day7)作为一个快照,而不是第一天(Day0)。你可以将每个快照视为代表过去 24 小时内最新的状态更新。换句话说,$Dayi$ 表示来自 [$Day{i-1}$ 00:00 UTC, $Day_{i-1}$ 23:59 UTC] 的最新状态。
由于快照四舍五入向上到最近的一天,除了 operator{'<>'}avs 取消注册和 operator{'<>'}operator set 取消注册外,同一天内的状态更新将收到完全相同的奖励金额。
例如,假设 stakers A 和 B 拥有等量的 stake,并且在以下时间选择加入同一个 operator,他们在 Day 2 rewardSnaphot
的奖励将是相等的:
Day1---------------------Day2
^ ^
A B
这是从每日快照计算奖励的一个已知副作用。
此外,退出系统也具有相同的属性。如果 Operators J 和 K 在同一天内从同一个 AVS 退出,他们都不会收到来自该 AVS 的奖励:
Day1---------------------Day2
^ ^
J K
如上文转换部分所述,一个已知的副作用是,operator可以通过在 2 个快照的边界处从 AVS 取消注册来失去一天的奖励。例如:
--------Day1---------------------Day2---------------------Day3
^ ^
Entry Exit
在上述情况下,即使 operator 仅验证了 AVS 近 2 天,该 operator 也会被计为在 Day1 注册,并在 Day2 取消注册。
每次奖励提交都有两个长度相等的数组:strategies
和 multiplier
。管道使用此值来计算快照奖励的参与者的 stakeWeight。对于给定的 staker $s$,在快照 $d$ 上,stakeWeight 由下式给出:
$stakeWeight_{s, d} = multiplieri \cdot shares{i,s,d}$
该计算也在 AVS 的 StakeRegistry
合约中完成。参考 solidity 实现。
一个关键的不变量是,对于给定的奖励提交 $r$,在奖励快照 $d$ 上,$Tokens{r,d} >= \sum{i=0}^{n=paidEarners} Earner_{i,r,d}$
换句话说,奖励提交的 tokensPerDay
不能小于分配给 rewardSnaphot
的所有参与者的奖励总和。我们将此作为从转换 shares 和 multipliers 为 double 类型时的截断的一个关键考虑因素,double 类型最多可容纳 15 个有效数字。
RewardsCoordinator
要求 CALCULATION_INTERVAL_SECONDS % SNAPSHOT_CADENCE == 0
,这保证了每个奖励快照都将位于奖励范围的边界内。
按照奖励更新者定义的某个频率,管道会将所有奖励分配快照聚合到某个时间戳 $t$。对于要成为“全新”的根,它必须对大于 lastRewardTimestamp
的 rewardSnaphot
之后的状态进行 merkleize。
如果 AVS 为没有重新stake策略的快照进行了奖励,则该奖励将不会重新分配到奖励提交的未来快照。 请参阅 奖励快照 operators 一个具体的例子。
以下每个查询都是 active_rewards
视图的一部分 CTE 集。
为了处理追溯奖励,我们寻找在 cutoff_date
之前开始的任何奖励。为了确保不重新计算奖励,我们限制计算活动奖励的快照范围。
下表将用于帮助可视化转换。 让我们假设管道的 cutoff_date
是 2024 年 4 月 29 日 0:00 UTC。 此外,假设上次奖励快照是 2024 年 4 月 24 日。注意:由于青铜表查询和截止日期,奖励事件需要 2 天才能显示在当前快照的黄金计算中。
AVS | Reward Hash | Start Timestamp | Duration | End Timestamp | Amount | Strategy | Multiplier |
---|---|---|---|---|---|---|---|
AVS1 | 0xReward1 | 4-21-2024 | 21 days | 5-12-2024 | 21e18 | stETH | 1e18 |
AVS1 | 0xReward1 | 4-21-2024 | 21 days | 5-12-2024 | 21e18 | rETH | 2e18 |
为简洁起见,每个示例中仅显示表的相应行。
WITH active_rewards_modified as (
SELECT *,
amount/(duration/86400) as tokens_per_day,
cast('{{ var("cutoff_date") }}' AS TIMESTAMP(6)) as global_end_inclusive -- Inclusive means we DO USE this day as a snapshot
FROM {{ ref('rewards_combined') }}
WHERE end_timestamp >= TIMESTAMP '{{ var("rewards_start") }}' and start_timestamp <= TIMESTAMP '{{ var("cutoff_date") }}'
),
tokens_per_day
。 这是在给定的 rewardSnapshot
中分配给所有参与者的 token 数量rewardsSnapshot
,因为对于每次奖励提交,我们可以在管道运行中计算多个快照的奖励global_end_inclusive
是此管道运行的最后一个快照,用于计算奖励。 它与 cutoff_date
相同end_timestamp
大于 UNIX 时间的开始时间,但在后续步骤中会得到适当处理Tokens Per Day | Global End Inclusive |
---|---|
1e18 | 4-27-2024 |
active_rewards_updated_end_timestamps as (
SELECT
avs,
/**
* Cut the start and end windows to handle
* A. Retroactive rewards that came recently whose start date is less than start_timestamp
* B. Don't make any rewards past end_timestamp for this run
*/
start_timestamp as reward_start_exclusive,
LEAST(global_end_inclusive, end_timestamp) as reward_end_inclusive,
tokens_per_day,
token,
multiplier,
strategy,
reward_type,
reward_for_all,
global_end_inclusive,
block_date as reward_submission_date
FROM active_rewards_modified
),
start_timestamp
被重命名为 reward_start_exclusive
。 它被标记为 exclusive,因为我们已经在之前的运行中在这个时间点拍摄了快照,或者它是奖励提交的第 0 天,并且不会拍摄任何快照(请参阅 计算范围)reward_end_inclusive
是 MIN(global_end_inclsuive, end_timestamp)
。 这会将 end_timestamp
限制为不大于给定运行的 cutoff_time
Reward Start Exclusive | Reward End Inclusive |
---|---|
4-21-2024 | 4-27-2024 |
-- For each reward hash, find the latest snapshot
active_rewards_updated_start_timestamps as (
SELECT
ap.avs,
CASE
WHEN '{{ var("is_backfill") }}' = 'true' THEN ap.reward_start_exclusive
ELSE COALESCE(MAX(g.snapshot), ap.reward_start_exclusive)
END as reward_start_exclusive,
ap.reward_end_inclusive,
ap.token,
ap.tokens_per_day as tokens_per_day_decimal,
-- Round down to 15 sigfigs for double precision, ensuring know errouneous round up or down
ap.tokens_per_day * ((POW(10, 15) - 1)/(POW(10, 15))) as tokens_per_day,
ap.multiplier,
ap.strategy,
ap.reward_hash,
ap.reward_type,
ap.global_end_inclusive,
ap.reward_submission_date
FROM active_rewards_updated_end_timestamps ap
LEFT JOIN {{ var('schema_name') }}.gold_table g
ON g.reward_hash = ap.reward_hash
GROUP BY ap.avs, ap.reward_end_inclusive, ap.token, ap.tokens_per_day, ap.multiplier, ap.strategy, ap.reward_hash, ap.global_end_inclusive, ap.reward_start_exclusive, ap.reward_for_all
),
根据来自最终黄金表的 reward_hash
的最新 snapshot
钳制 reward_start_exclusive
,该表包含每个 snapshotReward
。 我们这样做是为了不重新计算奖励。 如果黄金表中没有快照,那么我们只使用奖励提交的原始 reward_start_exclusive
。
此步骤还将 tokens_per_day
转换为 double,这将在其余的奖励计算中使用。 这使我们能够支持高达 1e38-1 的值,但代价是只有 15 个小数位的精度。
最后,如果运行是回填,我们将开始时间戳设置为最早的可能的 unix 时间戳 1970-01-01 00:00:00
。
让我们假设 reward_hash 的最新 snapshotReward
是 4-24-2024。reward_start_exclusive
的先前值是 4-21-2024。
Reward start exclusive | Reward end inclusive |
---|---|
4-24-2024 | 4-28-2024 |
active_reward_ranges AS (
SELECT * from active_rewards_updated_start_timestamps
/** Take out (reward_start_exclusive, reward_end_inclusive) windows where
* 1. reward_start_exclusive >= reward_end_inclusive: The reward period is done or we will handle on a subsequent run
*/
WHERE reward_start_exclusive < reward_end_inclusive
),
解析出无效范围。 如果当抢跑是回填,并且先抢跑的快照大于我们正在回填的 cutoff_time
,则可能会发生这种情况。
exploded_active_range_rewards AS (
SELECT * FROM active_reward_ranges
CROSS JOIN generate_series(DATE(reward_start_exclusive), DATE(reward_end_inclusive), INTERVAL '1' DAY) AS day
),
为奖励范围内的每个快照创建一行
AVS | Reward Hash | Day | Strategy | Multiplier | ... |
---|---|---|---|---|---|
AVS1 | 0xReward1 | 4-24-2024 | stETH | 1e18 | |
AVS1 | 0xReward1 | 4-25-2024 | stETH | 1e18 | |
AVS1 | 0xReward1 | 4-26-2024 | stETH | 1e18 | |
AVS1 | 0xReward1 | 4-27-2024 | stETH | 1e18 | |
AVS1 | 0xReward1 | 4-24-2024 | rETH | 2e18 | |
AVS1 | 0xReward1 | 4-25-2024 | rETH | 2e18 | |
AVS1 | 0xReward1 | 4-26-2024 | rETH | 2e18 | |
AVS1 | 0xReward1 | 4-27-2024 | rETH | 2e18 |
active_rewards_final AS (
SELECT
avs,
cast(day as DATE) as snapshot,
token,
tokens_per_day,
tokens_per_day_decimal,
multiplier,
strategy,
reward_hash,
reward_type,
reward_submission_date
FROM exploded_active_range_rewards
-- Remove snapshots on the start day
WHERE day != reward_start_exclusive
)
select * from active_rewards_final
删除快照等于奖励的 reward_start_exclusive 的行。
AVS | Reward Hash | Snapshot | Strategy | Multiplier | Reward Start Exclusive |
---|---|---|---|---|---|
<s>AVS1</s> | <s>0xReward1</s> | <s>4-24-2024</s> | <s>stETH</s> | <s>1e18</s> | <s>4-24-2024</s> |
AVS1 | 0xReward1 | 4-25-2024 | stETH | 1e18 | 4-24-2024 |
AVS1 | 0xReward1 | 4-26-2024 | stETH | 1e18 | 4-24-2024 |
AVS1 | 0xReward1 | 4-27-2024 | stETH | 1e18 | 4-24-2024 |
<s>AVS1</s> | <s>0xReward1</s> | <s>4-24-2024</s> | <s>rETH</s> | <s>2e18</s> | <s>4-24-2024</s> |
AVS1 | 0xReward1 | 4-25-2024 | rETH | 2e18 | 4-24-2024 |
AVS1 | 0xReward1 | 4-26-2024 | rETH | 2e18 | 4-24-2024 |
AVS1 | 0xReward1 | 4-27-2024 | rETH | 2e18 | 4-24-2024 |
生成有效奖励后,管道随后计算奖励分配给 staker operator set。
我们将奖励提交的 multiplier
和 staker 的 shares
转换为 double,以便对这些值进行计算。 double 类型有 15 个有效数字,这种不精确性反映在计算 staker 的 stakeWeight
、staker_proportion
、total_staker_operator_reward
和 staker_tokens
的 CTE 中。必须是这种情况,对于给定的奖励提交 $r$,$\sum{i=0}^{n=avsStakerOperatorSet} stakeroperatorSetReward{i, r} {'<='} tokensPerDay_r$。
我们首先计算奖励分配给整个 staker operator set,然后将其传递给 operators 和 stakers。
WITH reward_snapshot_operators as (
SELECT
ap.reward_hash,
ap.snapshot,
ap.token,
ap.tokens_per_day,
ap.tokens_per_day_decimal,
ap.avs,
ap.strategy,
ap.multiplier,
ap.reward_type,
ap.reward_submission_date,
oar.operator
FROM {{ ref('1_active_rewards') }} ap
JOIN {{ ref('operator_avs_registration_snapshots') }} oar
ON ap.avs = oar.avs and ap.snapshot = oar.snapshot
WHERE ap.reward_type = 'avs'
),
从 active_rewards
中,获取为 AVS 注册的 operators。我们过滤 reward_for_all = false
,因为我们只关注 AVS 奖励提交。
让我们假设 AVS 的 operator 注册快照是。 4-27-2024 是活动快照,但青铜表将没有来自此日期的事件(因为查询是针对所有事件 < cutoffDate
)。在此运行中,4-27-2024 不会支付给任何 staker 或 operator。这就是两个快照延迟的来源。
对于没有 operators 选择加入 AVS 的日子,1e18 的 tokens_per_day
不会重新分配到未来的 rewardSnapshots
。
Operator | AVS | Snapshot |
---|---|---|
Operator1 | AVS1 | 4-25-2024 |
Operator1 | AVS1 | 4-26-2024 |
Operator2 | AVS1 | 4-25-2024 |
Operator2 | AVS1 | 4-26-2024 |
_operator_restaked_strategies AS (
SELECT
rso.*
FROM reward_snapshot_operators rso
JOIN {{ ref('operator_avs_strategy_snapshots') }} oas
ON
rso.operator = oas.operator AND
rso.avs = oas.avs AND
rso.strategy = oas.strategy AND
rso.snapshot = oas.snapshot
),
从在 AVS 上重新stake的 operators 中,获取他们已在上面重新stake的每个 AVS 的策略和 shares 和 shares。
让我们假设 AVS 上每个 operator 的策略是:
Operator | AVS | Strategy | Snapshot |
---|---|---|---|
Operator1 | AVS1 | stETH | 4-25-2024 |
Operator1 | AVS1 | stETH | 4-26-2024 |
Operator1 | AVS1 | rETH | 4-26-2024 |
Operator2 | AVS1 | stETH | 4-25-2024 |
Operator2 | AVS1 | stETH | 4-26-2024 |
staker_delegated_operators AS (
SELECT
ors.*,
sds.staker
FROM _operator_restaked_strategies ors
JOIN {{ ref('staker_delegation_snapshots') }} sds
ON
ors.operator = sds.operator AND
ors.snapshot = sds.snapshot
),
获取为快照委托给 operator 的 stakers。
Operator | AVS | Strategy | Snapshot | Staker |
---|---|---|---|---|
Operator1 | AVS1 | stETH | 4-25-2024 | Staker1 |
Operator1 | AVS1 | stETH | 4-26-2024 | Staker1 |
Operator1 | AVS1 | stETH | 4-26-2024 | Staker2 |
Operator1 | AVS1 | rETH | 4-26-2024 | Staker1 |
Operator1 | AVS1 | rETH | 4-26-2024 | Staker2 |
Operator2 | AVS1 | stETH | 4-25-2024 | Staker3 |
Operator2 | AVS1 | stETH | 4-26-2024 | Staker3 |
staker_avs_strategy_shares AS (
SELECT
sdo.*,
sss.shares
FROM staker_delegated_operators sdo
JOIN {{ ref('staker_share_snapshots') }} sss
ON
sdo.staker = sss.staker AND
sdo.snapshot = sss.snapshot AND
sdo.strategy = sss.strategy
-- Parse out negative shares and zero multiplier so there is no division by zero case
WHERE sss.shares > 0 and sdo.multiplier != 0
),
让我们假设 stakers 具有以下状态:
Staker | Strategy | Shares | Snapshot |
---|---|---|---|
Staker1 | stETH | 1e18 | 4-25-2024 |
Staker1 | stETH | 1e18 | 4-26-2024 |
Staker1 | rETH | 2e18 | 4-26-2024 |
Staker2 | stETH | 1e18 | 4-26-2024 |
Staker3 | stETH | 2e18 | 4-25-2024 |
Staker3 | stETH | 2e18 | 4-26-2024 |
连接将产生以下结果:
Operator | AVS | Strategy | Snapshot | Staker | Shares |
---|---|---|---|---|---|
Operator1 | AVS1 | stETH | 4-25-2024 | Staker1 | 1e18 |
Operator1 | AVS1 | stETH | 4-26-2024 | Staker1 | 1e18 |
Operator1 | AVS1 | rETH | 4-26-2024 | Staker1 | 2e18 |
Operator1 | AVS1 | stETH | 4-26-2024 | Staker2 | 1e18 |
Operator2 | AVS1 | stETH | 4-25-2024 | Staker3 | 2e18 |
Operator2 | AVS1 | stETH | 4-26-2024 | Staker3 | 2e18 |
Staker2 没有 rETH
shares,这就是为什么此视图的行数少 1 行。
staker_weights AS (
SELECT *,
SUM(multiplier * shares) OVER (PARTITION BY staker, reward_hash, snapshot) AS staker_weight
FROM staker_avs_strategy_shares
),
计算 staker 的 stakeWeight
。有关此计算的讨论请参见上文。
Operator | AVS | Strategy | Snapshot | Staker | Shares | Multiplier | Staker Weight |
---|---|---|---|---|---|---|---|
Operator1 | AVS1 | stETH | 4-25-2024 | Staker1 | 1e18 | 1e18 | 1e36 |
Operator1 | AVS1 | stETH | 4-26-2024 | Staker1 | 1e18 | 1e18 | 3e36 |
Operator1 | AVS1 | rETH | 4-26-2024 | Staker1 | 1e18 | 2e18 | 3e36 |
Operator1 | AVS1 | stETH | 4-26-2024 | Staker2 | 1e18 | 1e18 | 1e36 |
Operator2 | AVS1 | stETH | 4-25-2024 | Staker3 | 2e18 | 1e18 | 2e36 |
Operator2 | AVS1 | stETH | 4-26-2024 | Staker3 | 2e18 | 1e18 | 2e36 |
distinct_stakers AS (
SELECT *
FROM (
SELECT *,
-- We can use an arbitrary order here since the staker_weight is the same for each (staker, strategy, hash, snapshot)
-- We use strategy ASC for better debuggability
ROW_NUMBER() OVER (PARTITION BY reward_hash, snapshot, staker ORDER BY strategy ASC) as rn
FROM staker_weights
) t
WHERE rn = 1
ORDER BY reward_hash, snapshot, staker
),
在之前的计算中,stake weight 是按 (reward_hash
, staker
, snapshot
) 计算的。 我们需要删除任何具有相同组合的行,因为下一步是计算每个快照的总 staker weight。 strategy 列是不相关的,因为最终奖励由 $tokensPerDay * stakerWeightProportion$ 给出。 我们添加 order by 子句,以便下一步中 staker weight 的总和是确定性的。
删除具有相同 (staker
, reward_hash
, snapshot
) 的行
Operator | AVS | Strategy | Snapshot | Staker | Shares | Multiplier | Staker Weight |
---|---|---|---|---|---|---|---|
Operator1 | AVS1 | stETH | 4-25-2024 | Staker1 | 1e18 | 1e18 | 1e36 |
Operator1 | AVS1 | stETH | 4-26-2024 | Staker1 | 1e18 | 1e18 | 3e36 |
<s> Operator1 </s> | <s> AVS1 </s> | <s> rETH </s> | <s> 4-26-2024 </s> | <s> Staker1 </s> | <s> 1e18 </s> | <s> 2e18 </s> | <s> 3e36 </s> |
Operator1 | AVS1 | stETH | 4-26-2024 | Staker2 | 1e18 | 1e18 | 1e36 |
Operator2 | AVS1 | stETH | 4-25-2024 | Staker3 | 2e18 | 1e18 | 2e36 |
Operator2 | AVS1 | stETH | 4-26-2024 | Staker3 | 2e18 | 1e18 | 2e36 |
staker_weight_sum AS (
SELECT *,
SUM(staker_weight) OVER (PARTITION BY reward_hash, snapshot) as total_weight
FROM distinct_stakers
),
获取给定 (reward_hash
, snapshot
) 的所有 staker weight 的总和
Operator | AVS | Strategy | Snapshot | Staker | Shares | Staker Weight | Total Staker Weight |
---|---|---|---|---|---|---|---|
Operator1 | AVS1 | stETH | 4-25-2024 | Staker1 | 1e18 | 1e36 | 3e36 |
Operator1 | AVS1 | stETH | 4-26-2024 | Staker1 | 1e18 | 3e36 | 6e36 |
Operator1 | AVS1 | stETH | 4-26-2024 | Staker2 | 1e18 | 1e36 | 6e36 |
Operator2 | AVS1 | stETH | 4-25-2024 | Staker3 | 2e18 | 2e36 | 3e36 |
Operator2 | AVS1 | stETH | 4-26-2024 | Staker3 | 2e18 | 2e36 | 6e36 |
staker_proportion AS (
SELECT *,
FLOOR((staker_weight / total_weight) * 1000000000000000) / 1000000000000000 AS staker_proportion
FROM staker_weight_sum
),
计算 snapshotReward 的 token 的 staker 比例我们向下舍入 staker_proportion 以确保 staker_proportions 的总和不大于 1。 |
运算符 (Operator) | AVS | 策略 (Strategy) | 快照 (Snapshot) | 质押者 (Staker) | 份额 (Shares) | 质押者权重 (Staker Weight) | 总质押者权重 (Total Staker Weight) | 质押者比例 (Staker Proportion) |
---|---|---|---|---|---|---|---|---|---|
Operator1 | AVS1 | stETH | 4-25-2024 | Staker1 | 1e18 | 1e36 | 3e36 | 0.333 | |
Operator1 | AVS1 | stETH | 4-26-2024 | Staker1 | 1e18 | 3e36 | 6e36 | 0.5 | |
Operator1 | AVS1 | stETH | 4-26-2024 | Staker2 | 1e18 | 1e36 | 6e36 | 0.166 | |
Operator2 | AVS1 | stETH | 4-25-2024 | Staker3 | 2e18 | 2e36 | 3e36 | 0.666 | |
Operator2 | AVS1 | stETH | 4-26-2024 | Staker3 | 2e18 | 2e36 | 6e36 | 0.333 |
staker_operator_total_tokens AS (
SELECT *,
CASE
-- 对于在硬分叉之前的快照,并且在硬分叉之前提交,我们使用旧的计算方法
WHEN snapshot < '{{ var("amazon_hard_fork") }}' AND reward_submission_date < '{{ var("amazon_hard_fork") }}' THEN
cast(staker_proportion * tokens_per_day AS DECIMAL(38,0))
WHEN snapshot < '{{ var("nile_hard_fork") }}' AND reward_submission_date < '{{ var("nile_hard_fork") }}' THEN
(staker_proportion * tokens_per_day)::text::decimal(38,0)
ELSE
FLOOR(staker_proportion * tokens_per_day_decimal)
END as total_staker_operator_payout
FROM staker_proportion
),
我们已经进行了两次硬分叉:
这段代码反映了处理这个边界情况,以使回填情况下的计算具有幂等性。(This code reflects handling this edge case to make calculations idempotent in the cases of a backfill.)
计算将支付给所有质押者和运营者的代币数量。我们将结果从文本转换为 DECIMAL(38,0)
以不损失任何精度。此外,对于给定的快照,代币值适合此类型。(Calculate the number of tokens that will be paid out to all stakers and operators. We cast the result from text to DECIMAL(38,0)
to not lose any precision. Furthemore, the token value fits within this type for a given snapshot.)
运算符 (Operator) | AVS | 策略 (Strategy) | 快照 (Snapshot) | 质押者 (Staker) | 份额 (Shares) | 质押者权重 (Staker Weight) | 总质押者权重 (Total Staker Weight) | 质押者比例 (Staker Proportion) | 每日代币 (Tokens Per Day) | 总质押者运算符奖励 (Total Staker Operator Reward) |
---|---|---|---|---|---|---|---|---|---|---|
Operator1 | AVS1 | stETH | 4-25-2024 | Staker1 | 1e18 | 1e36 | 3e36 | 0.333 | 1e18 | 333333333333333000 |
Operator1 | AVS1 | stETH | 4-26-2024 | Staker1 | 1e18 | 3e36 | 6e36 | 0.5 | 1e18 | 5e17 |
Operator1 | AVS1 | stETH | 4-26-2024 | Staker2 | 1e18 | 1e36 | 6e36 | 0.166 | 1e18 | 166666666666666000 |
Operator2 | AVS1 | stETH | 4-25-2024 | Staker3 | 2e18 | 2e36 | 3e36 | 0.666 | 1e18 | 666666666666666000 |
Operator2 | AVS1 | stETH | 4-26-2024 | Staker3 | 2e18 | 2e36 | 6e36 | 0.333 | 1e18 | 333333333333333000 |
token_breakdowns AS (
SELECT *,
CASE
WHEN snapshot < '{{ var("amazon_hard_fork") }}' AND reward_submission_date < '{{ var("amazon_hard_fork") }}' THEN
cast(total_staker_operator_payout * 0.10 AS DECIMAL(38,0))
WHEN snapshot < DATE '{{ var("nile_hard_fork") }}' AND reward_submission_date < '{{ var("nile_hard_fork") }}' THEN
(total_staker_operator_payout * 0.10)::text::decimal(38,0)
ELSE
floor(total_staker_operator_payout * 0.10)
END as operator_tokens,
CASE
WHEN snapshot < '{{ var("amazon_hard_fork") }}' AND reward_submission_date < '{{ var("amazon_hard_fork") }}' THEN
total_staker_operator_payout - cast(total_staker_operator_payout * 0.10 as DECIMAL(38,0))
WHEN snapshot < '{{ var("nile_hard_fork") }}' AND reward_submission_date < '{{ var("nile_hard_fork") }}' THEN
total_staker_operator_payout - ((total_staker_operator_payout * 0.10)::text::decimal(38,0))
ELSE
total_staker_operator_payout - floor(total_staker_operator_payout * 0.10)
END as staker_tokens
FROM staker_operator_total_tokens
)
SELECT * from token_breakdowns
ORDER BY reward_hash, snapshot, staker, operator
计算欠运营商和质押者的代币数量。运营商获得固定的 10% 佣金。从文本转换为 DECIMAL(38,0)
将仅截断数字的小数部分(即,不会有四舍五入)。我们添加 order by 子句是为了使下一个查询中质押者权重的总和是确定性的。(Calculate the number of tokens owed to the operator and to the staker. Operators get a fixed 10% commission. Casting from text to DECIMAL(38,0)
will only truncate the decimal proportion of the number (ie. there will be no rounding). We add the order by clause in order to have the sum of staker weight in the next query be deterministic.)
运算符 (Operator) | AVS | 策略 (Strategy) | 快照 (Snapshot) | 质押者 (Staker) | 份额 (Shares) | 质押者权重 (Staker Weight) | 总质押者权重 (Total Staker Weight) | 质押者比例 (Staker Proportion) | 每日代币 (Tokens Per Day) | 总质押者运算符奖励 (Total Staker Operator Reward) | 运算符代币 (Operator Tokens) | 质押者代币 (Staker Tokens) |
---|---|---|---|---|---|---|---|---|---|---|---|---|
Operator1 | AVS1 | stETH | 4-25-2024 | Staker1 | 1e18 | 1e36 | 3e36 | 0.333 | 1e18 | 333333333333333000 | 33333333333333300 | 299999999999999700 |
Operator1 | AVS1 | stETH | 4-26-2024 | Staker1 | 1e18 | 3e36 | 6e36 | 0.5 | 1e18 | 5e17 | 5e16 | 4.5e17 |
Operator1 | AVS1 | stETH | 4-26-2024 | Staker2 | 1e18 | 1e36 | 6e36 | 0.166 | 1e18 | 166666666666666000 | 16666666666666600 | 149999999999999400 |
Operator2 | AVS1 | stETH | 4-25-2024 | Staker3 | 2e18 | 2e36 | 3e36 | 0.666 | 1e18 | 666666666666666000 | 66666666666666600 | 599999999999999400 |
Operator2 | AVS1 | stETH | 4-26-2024 | Staker3 | 2e18 | 2e36 | 6e36 | 0.333 | 1e18 | 333333333333333000 | 33333333333333300 | 299999999999999700 |
我们可以从其质押者的奖励分配总和来计算运算符奖励分配,因为运算符 $o$ 的份额由下式给出:
$Shares{o} = \sum{i=0}^{n=operatorStakers} Shares_i$
WITH operator_token_sums AS (
SELECT
reward_hash,
snapshot,
token,
tokens_per_day,
avs,
strategy,
multiplier,
reward_type,
operator,
SUM(operator_tokens) OVER (PARTITION BY operator, reward_hash, snapshot) AS operator_tokens
FROM {{ ref('2_staker_reward_amounts') }}
),
获取给定 reward_hash
和 snapshot
的每个运算符的总和。每天的代币是1e18
。如果我们在4-25-26
上进行运算符奖励分配,则大约是 1e18 的 10%。此外,第 2 行和第 3 行是等效的。(Gets the sum for each operator for a given reward_hash
and snapshot
. The tokens per day was 1e18
. If we take the operators reward distribution on 4-25-26
, that is roughly 10% of 1e18. In addition, the rows 2 and 3 are equivalent.)
运算符 (Operator) | AVS | 策略 (Strategy) | 快照 (Snapshot) | 质押者 (Staker) | 运算符代币 (Operator Tokens) | 质押者代币 (Staker Tokens) | 运算符代币 (总和) (Operator Tokens (Sum)) |
---|---|---|---|---|---|---|---|
Operator1 | AVS1 | stETH | 4-25-2024 | Staker1 | 33333333333333300 | 299999999999999700 | 33333333333333300 |
Operator1 | AVS1 | stETH | 4-26-2024 | Staker1 | 5e16 | 4.5e17 | 66666666666666600 |
Operator1 | AVS1 | stETH | 4-26-2024 | Staker2 | 16666666666666600 | 149999999999999400 | 66666666666666600 |
Operator2 | AVS1 | stETH | 4-25-2024 | Staker3 | 66666666666666600 | 599999999999999400 | 66666666666666600 |
Operator2 | AVS1 | stETH | 4-26-2024 | Staker3 | 33333333333333300 | 299999999999999700 | 33333333333333300 |
distinct_operators AS (
SELECT *
FROM (
SELECT *,
-- 我们可以使用任意顺序,因为每个(运算符、策略、哈希、快照)的 staker_weight 都相同
-- 我们使用 strategy ASC 以获得更好的可调试性
ROW_NUMBER() OVER (PARTITION BY reward_hash, snapshot, operator ORDER BY strategy ASC) as rn
FROM operator_token_sums
) t
WHERE rn = 1
)
SELECT * FROM distinct_operators
在上一步中,我们汇总了具有相同snapshot
和reward_hash
的运算符奖励。现在我们删除这些行,以便在最终奖励分配中,每个reward_hash
和snapshot
仅对运算符计数一次。(In the previous step, we aggregated operator rewards with the same snapshot
and reward_hash
. Now we remove these rows so the operator is only counted once per reward_hash
and snapshot
in the final reward distribution.)
运算符 (Operator) | AVS | 策略 (Strategy) | 快照 (Snapshot) | 质押者 (Staker) | 运算符代币 (Operator Tokens) | 质押者代币 (Staker Tokens) | 运算符代币 (总和) (Operator Tokens (Sum)) |
---|---|---|---|---|---|---|---|
Operator1 | AVS1 | stETH | 4-25-2024 | Staker1 | 33333333333333300 | 299999999999999700 | 33333333333333300 |
<s> Operator1 </s> | <s> AVS1 </s> | <s> stETH </s> | <s> 4-26-2024 </s> | <s> Staker1 </s> | <s> 5e16 </s> | <s> 4.5e17 </s> | <s> 66666666666666600 </s> |
Operator1 | AVS1 | stETH | 4-26-2024 | Staker2 | 16666666666666600 | 149999999999999400 | 66666666666666600 |
Operator2 | AVS1 | stETH | 4-25-2024 | Staker3 | 66666666666666600 | 599999999999999400 | 66666666666666600 |
Operator2 | AVS1 | stETH | 4-26-2024 | Staker3 | 33333333333333300 | 299999999999999700 | 33333333333333300 |
此查询计算通过RewardCoordinator
上的createRewardsForAllSubmission
函数获得的奖励。此奖励直接发给质押者。(This query calculates rewards made by via the createRewardsForAllSubmission
function on the RewardCoordinator
. This reward goes directly to stakers.)
注意:此功能目前已暂停且未使用,因此不存在硬分叉逻辑。(Note: This functionality is currently paused and is unused, hence no hardfork logic is present.)
WITH reward_snapshot_stakers AS (
SELECT
ap.reward_hash,
ap.snapshot,
ap.token,
ap.tokens_per_day,
ap.avs,
ap.strategy,
ap.multiplier,
ap.reward_type,
sss.staker,
sss.shares
FROM {{ ref('1_active_rewards') }} ap
JOIN {{ ref('staker_share_snapshots') }} as sss
ON ap.strategy = sss.strategy and ap.snapshot = sss.snapshot
WHERE ap.reward_type = 'all_stakers'
-- 解析出负份额和零乘数,因此不存在除以零的情况
AND sss.shares > 0 and ap.multiplier != 0
),
选择所有将reward_for_all
设置为 true 的奖励 (Select all the rewards that set reward_for_all
to true)
计算与 步骤 2 相同,除了我们不需要检查运算符 a 质押者是否已委托 (The calculation is the same as step 2, except we do not need to check the operator a stakers is delegated to)
-- 计算质押者的权重
staker_weights AS (
SELECT *,
SUM(multiplier * shares) OVER (PARTITION BY staker, reward_hash, snapshot) AS staker_weight
FROM reward_snapshot_stakers
),
-- 由于质押者的权重已经计算出来,因此获取不同的质押者
distinct_stakers AS (
SELECT *
FROM (
SELECT *,
-- 我们可以使用任意顺序,因为每个(质押者、策略、哈希、快照)的 staker_weight 都相同
ROW_NUMBER() OVER (PARTITION BY reward_hash, snapshot, staker ORDER BY strategy ASC) as rn
FROM staker_weights
) t
WHERE rn = 1
ORDER BY reward_hash, snapshot, staker
),
-- 计算所有质押者权重的总和
staker_weight_sum AS (
SELECT *,
SUM(staker_weight) OVER (PARTITION BY reward_hash, snapshot) as total_staker_weight
FROM distinct_stakers
),
-- 计算质押者代币比例
staker_proportion AS (
SELECT *,
FLOOR((staker_weight / total_staker_weight) * 1000000000000000) / 1000000000000000 AS staker_proportion
FROM staker_weight_sum
),
-- 计算质押者的总代币
staker_tokens AS (
SELECT *,
(tokens_per_day * staker_proportion)::text::decimal(38,0) as staker_tokens
FROM staker_proportion
)
SELECT * from staker_tokens
此奖励功能奖励所有选择加入至少一个 AVS 的运营商(及其委托的质押者)。运营商获得固定的 10% 佣金。(This reward functionality rewards all operators (and their delegated stakers) who have opted into at least one AVS. The operator gets a fixed 10% commission.)
我们通过以下方式做到这一点:
WITH avs_opted_operators AS (
SELECT DISTINCT
snapshot,
operator
FROM {{ ref('operator_avs_registration_snapshots') }}
),
获取已注册给定快照的 AVS 的唯一运算符。这使用operator_avs_registration_snapshots
预计算视图。请注意,此表中不存在取消注册。(Gets the unique operators who have registered for an AVS for a given snapshot. This uses the operator_avs_registration_snapshots
preclalculation view. Note that deregistrations do not exist in this table.)
-- 获取将在给定快照的奖励提交中获得奖励的运算符
reward_snapshot_operators as (
SELECT
ap.reward_hash,
ap.snapshot,
ap.token,
ap.tokens_per_day_decimal,
ap.avs,
ap.strategy,
ap.multiplier,
ap.reward_type,
ap.reward_submission_date,
aoo.operator
FROM {{ ref('1_active_rewards') }} ap
JOIN avs_opted_operators aoo
ON ap.snapshot = aoo.snapshot
WHERE ap.reward_type = 'all_earners'
),
我们将活动奖励与快照中已注册至少一个 AVS 的运算符连接起来。(We add join the active rewards with operators who have registered to at least one AVS for the snapshot.)
-- 获取已委托给快照的运算符的质押者
staker_delegated_operators AS (
SELECT
rso.*,
sds.staker
FROM reward_snapshot_operators rso
JOIN {{ ref('staker_delegation_snapshots') }} sds
ON
rso.operator = sds.operator AND
rso.snapshot = sds.snapshot
),
获取已委托给快照的注册运算符的质押者。(Get the stakers that were delegated to the registered operator for the snapshot.)
其余计算与2_staker_reward_amounts
相同,没有硬分叉。(The rest of the calculation proceeds as 2_staker_reward_amounts
, without having hard forks.)
-- 获取质押者已委托给运算符的每种策略的份额
staker_strategy_shares AS (
SELECT
sdo.*,
sss.shares
FROM staker_delegated_operators sdo
JOIN {{ ref('staker_share_snapshots') }} sss
ON
sdo.staker = sss.staker AND
sdo.snapshot = sss.snapshot AND
sdo.strategy = sss.strategy
-- 解析出负份额和零乘数,因此不存在除以零的情况
WHERE sss.shares > 0 and sdo.multiplier != 0
),
-- 计算质押者的权重
staker_weights AS (
SELECT *,
SUM(multiplier * shares) OVER (PARTITION BY staker, reward_hash, snapshot) AS staker_weight
FROM staker_strategy_shares
),
-- 由于质押者的权重已经计算出来,因此获取不同的质押者
distinct_stakers AS (
SELECT *
FROM (
SELECT *,
-- 我们可以使用任意顺序,因为每个(质押者、策略、哈希、快照)的 staker_weight 都相同
-- 我们使用 strategy ASC 以获得更好的可调试性
ROW_NUMBER() OVER (PARTITION BY reward_hash, snapshot, staker ORDER BY strategy ASC) as rn
FROM staker_weights
) t
WHERE rn = 1
ORDER BY reward_hash, snapshot, staker
),
-- 计算每个奖励和快照的所有质押者权重的总和
staker_weight_sum AS (
SELECT *,
SUM(staker_weight) OVER (PARTITION BY reward_hash, snapshot) as total_weight
FROM distinct_stakers
),
-- 计算每个奖励和快照的质押者代币比例
staker_proportion AS (
SELECT *,
FLOOR((staker_weight / total_weight) * 1000000000000000) / 1000000000000000 AS staker_proportion
FROM staker_weight_sum
),
-- 计算(质押者、运算符)对的总代币
staker_operator_total_tokens AS (
SELECT *,
FLOOR(staker_proportion * tokens_per_day_decimal) as total_staker_operator_payout
FROM staker_proportion
),
-- 计算每个(质押者、运算符)对的代币分解
token_breakdowns AS (
SELECT *,
floor(total_staker_operator_payout * 0.10) as operator_tokens,
total_staker_operator_payout - floor(total_staker_operator_payout * 0.10) as staker_tokens
FROM staker_operator_total_tokens
)
SELECT * from token_breakdowns
ORDER BY reward_hash, snapshot, staker, operator
与步骤 3 类似,我们现在必须解析出运算符的奖励。(Similar to step 3, we now have to parse out the reward for operators.)
-- 对于每个运算符、奖励哈希和快照,汇总运算符在质押者中的代币
WITH operator_token_sums AS (
SELECT
reward_hash,
snapshot,
token,
tokens_per_day_decimal,
avs,
strategy,
multiplier,
reward_type,
operator,
SUM(operator_tokens) OVER (PARTITION BY operator, reward_hash, snapshot) AS operator_tokens
FROM {{ ref('5_rfae_stakers') }}
),
-- 对于每个运算符、奖励哈希和快照,对策略中的运算符代币进行去重
distinct_operators AS (
SELECT *
FROM (
SELECT *,
-- 我们可以使用任意顺序,因为每个(运算符、策略、哈希、快照)的 staker_weight 都相同
-- 我们使用 strategy ASC 以获得更好的可调试性
ROW_NUMBER() OVER (PARTITION BY reward_hash, snapshot, operator ORDER BY strategy ASC) as rn
FROM operator_token_sums
) t
WHERE rn = 1
)
SELECT * FROM distinct_operators
WITH
-- 步骤 2:修改活动奖励并计算每天的代币
active_rewards_modified AS (
SELECT
*,
CAST(@cutoffDate AS TIMESTAMP(6)) AS global_end_inclusive -- Inclusive 表示我们确实使用这一天作为快照
FROM operator_directed_rewards
WHERE end_timestamp >= TIMESTAMP '{{.rewardsStart}}'
AND start_timestamp <= TIMESTAMP '{{.cutoffDate}}'
AND block_time <= TIMESTAMP '{{.cutoffDate}}' -- 始终确保我们不使用未来数据。由于我们从不回填,因此永远不会发生,但为了安全和一致性,我们在此处进行操作。
),
-- 步骤 3:削减每个奖励的开始和结束窗口以处理全局范围
active_rewards_updated_end_timestamps AS (
SELECT
avs,
operator,
/**
* 削减开始和结束窗口以处理
* A. 最近出现的追溯奖励,其开始日期小于 start_timestamp
* B. 不要在此运行的 end_timestamp 之后进行任何奖励
*/
start_timestamp AS reward_start_exclusive,
LEAST(global_end_inclusive, end_timestamp) AS reward_end_inclusive,
amount,
token,
multiplier,
strategy,
reward_hash,
duration,
global_end_inclusive,
block_date AS reward_submission_date
FROM active_rewards_modified
),
-- 步骤 4:对于每个奖励哈希,找到最新的快照
active_rewards_updated_start_timestamps AS (
SELECT
ap.avs,
ap.operator,
COALESCE(MAX(g.snapshot), ap.reward_start_exclusive) AS reward_start_exclusive,
ap.reward_end_inclusive,
ap.token,
-- 我们使用 floor 来确保我们始终低估每天的总代币
FLOOR(ap.amount) AS amount_decimal,
ap.multiplier,
ap.strategy,
ap.reward_hash,
ap.duration,
ap.global_end_inclusive,
ap.reward_submission_date
FROM active_rewards_updated_end_timestamps ap
LEFT JOIN gold_table g
ON g.reward_hash = ap.reward_hash
GROUP BY
ap.avs,
ap.operator,
ap.reward_end_inclusive,
ap.token,
ap.amount,
ap.multiplier,
ap.strategy,
ap.reward_hash,
ap.duration,
ap.global_end_inclusive,
ap.reward_start_exclusive,
ap.reward_submission_date
),
-- 步骤 5:过滤掉无效的奖励范围
active_reward_ranges AS (
/** 删除 (reward_start_exclusive, reward_end_inclusive) 窗口,其中
* 1. reward_start_exclusive >= reward_end_inclusive:奖励期已完成,或者我们将在后续运行中处理
*/
SELECT *
FROM active_rewards_updated_start_timestamps
WHERE reward_start_exclusive < reward_end_inclusive
),
-- 步骤 6:展开范围以获得每个包含日期的每日
exploded_active_range_rewards AS (
SELECT
*
FROM active_reward_ranges
CROSS JOIN generate_series(
DATE(reward_start_exclusive),
DATE(reward_end_inclusive),
INTERVAL '1' DAY
) AS day
),
-- 步骤 7:准备清理后的活动奖励
active_rewards_cleaned AS (
SELECT
avs,
operator,
CAST(day AS DATE) AS snapshot,
token,
amount_decimal,
multiplier,
strategy,
duration,
reward_hash,
reward_submission_date
FROM exploded_active_range_rewards
-- 删除开始日期的快照
WHERE day != reward_start_exclusive
),
-- 步骤 8:通过 (avs, snapshot, operator, reward_hash) 删除活动奖励
active_rewards_reduced_deduped AS (
SELECT DISTINCT avs, snapshot, operator, reward_hash
FROM active_rewards_cleaned
),
-- 步骤 9:除以运算符注册的快照数
op_avs_num_registered_snapshots AS (
SELECT
ar.reward_hash,
ar.operator,
COUNT(*) AS num_registered_snapshots
FROM active_rewards_reduced_deduped ar
JOIN operator_avs_registration_snapshots oar
ON
ar.avs = oar.avs
AND ar.snapshot = oar.snapshot
AND ar.operator = oar.operator
GROUP BY ar.reward_hash, ar.operator
),
-- 步骤 10:按运算符注册的快照数划分要支付的金额
active_rewards_with_registered_snapshots AS (
SELECT
arc.*,
COALESCE(nrs.num_registered_snapshots, 0) as num_registered_snapshots
FROM active_rewards_cleaned arc
LEFT JOIN op_avs_num_registered_snapshots nrs
ON
arc.reward_hash = nrs.reward_-- 计算每个 operator reward 的总 staker 分配,具有动态分配逻辑
-- 如果未找到分配,则默认为 1000 (10%)
```sql
staker_splits AS (
SELECT
rso.*,
CASE
WHEN rso.snapshot < @trinityHardforkDate AND rso.reward_submission_date < @trinityHardforkDate THEN
rso.tokens_per_registered_snapshot_decimal - FLOOR(rso.tokens_per_registered_snapshot_decimal * COALESCE(oas.split, 1000) / CAST(10000 AS DECIMAL))
ELSE
rso.tokens_per_registered_snapshot_decimal - FLOOR(rso.tokens_per_registered_snapshot_decimal * COALESCE(oas.split, dos.split, 1000) / CAST(10000 AS DECIMAL))
END AS staker_split
FROM reward_snapshot_operators rso
LEFT JOIN operator_avs_split_snapshots oas
ON rso.operator = oas.operator
AND rso.avs = oas.avs
AND rso.snapshot = oas.snapshot
LEFT JOIN default_operator_split_snapshots dos ON (rso.snapshot = dos.snapshot)
),
-- 获取为 snapshot 委托给 operator 的 staker
staker_delegated_operators AS (
SELECT
ors.*,
sds.staker
FROM staker_splits ors
JOIN staker_delegation_snapshots sds
ON ors.operator = sds.operator
AND ors.snapshot = sds.snapshot
),
-- 获取委托给 operator 的 staker 的 shares
staker_avs_strategy_shares AS (
SELECT
sdo.*,
sss.shares
FROM staker_delegated_operators sdo
JOIN staker_share_snapshots sss
ON sdo.staker = sss.staker
AND sdo.snapshot = sss.snapshot
AND sdo.strategy = sss.strategy
-- 过滤掉负 shares 和零 multiplier,以避免除以零
WHERE sss.shares > 0 AND sdo.multiplier != 0
),
-- 计算每个 staker 的权重
staker_weights AS (
SELECT
*,
SUM(multiplier * shares) OVER (PARTITION BY staker, reward_hash, snapshot) AS staker_weight
FROM staker_avs_strategy_shares
),
-- 获取不同的 staker,因为他们的权重已经计算出来了
distinct_stakers AS (
SELECT *
FROM (
SELECT
*,
-- 这里我们可以使用任意顺序,因为每个(staker、strategy、hash、snapshot)的 staker_weight 都是相同的
-- 我们使用 strategy ASC 以便更好地进行调试
ROW_NUMBER() OVER (
PARTITION BY reward_hash, snapshot, staker
ORDER BY strategy ASC
) AS rn
FROM staker_weights
) t
WHERE rn = 1
ORDER BY reward_hash, snapshot, staker
),
-- 计算每个 reward 和 snapshot 的所有 staker 权重的总和
staker_weight_sum AS (
SELECT
*,
SUM(staker_weight) OVER (PARTITION BY reward_hash, operator, snapshot) AS total_weight
FROM distinct_stakers
),
-- 计算每个 reward 和 snapshot 的 tokens 的 staker 比例
staker_proportion AS (
SELECT
*,
FLOOR((staker_weight / total_weight) * 1000000000000000) / 1000000000000000 AS staker_proportion
FROM staker_weight_sum
),
-- 计算 staker reward 金额
staker_reward_amounts AS (
SELECT
*,
FLOOR(staker_proportion * staker_split) AS staker_tokens
FROM staker_proportion
)
-- 输出最终表
SELECT * FROM staker_reward_amounts
-- 步骤 1:获取 operator 尚未注册 AVS 或 AVS 不存在的行
WITH reward_snapshot_operators AS (
SELECT
ap.reward_hash,
ap.snapshot AS snapshot,
ap.token,
ap.tokens_per_registered_snapshot_decimal,
ap.avs AS avs,
ap.operator AS operator,
ap.strategy,
ap.multiplier,
ap.reward_submission_date
FROM {{.activeODRewardsTable}} ap
WHERE
ap.num_registered_snapshots = 0
),
-- 步骤 2:为每个 (operator, reward hash, snapshot) 删除跨 strategies 的 operator tokens -- 由于以上结果是一个跨 strategies 的扁平化的 operator-directed reward 提交
distinct_operators AS (
SELECT *
FROM (
SELECT
*,
-- 这里我们可以使用任意顺序,因为每个 (operator, strategy, hash, snapshot) 的 avs_tokens 都是相同的
-- 我们使用 strategy ASC 以便更好地进行调试
ROW_NUMBER() OVER (
PARTITION BY reward_hash, snapshot, operator
ORDER BY strategy ASC
) AS rn
FROM reward_snapshot_operators
) t
WHERE rn = 1
),
-- 步骤 3:求和每个(reward hash、snapshot)的 operator tokens -- 因为我们想要将这些 operator 金额的总和退还给该 snapshot 的 reward 提交中的 AVS
operator_token_sums AS (
SELECT
reward_hash,
snapshot,
token,
avs,
operator,
SUM(tokens_per_registered_snapshot_decimal) OVER (PARTITION BY reward_hash, snapshot) AS avs_tokens
FROM distinct_operators
)
-- 步骤 4:输出最终表
SELECT * FROM operator_token_sums
WITH
-- 步骤 1:修改 active rewards 并计算每天的 tokens
active_rewards_modified AS (
SELECT
*,
CAST(@cutoffDate AS TIMESTAMP(6)) AS global_end_inclusive -- Inclusive(包含性)意味着我们**使用**这一天作为一个快照
FROM operator_directed_operator_set_rewards
WHERE end_timestamp >= TIMESTAMP '{{.rewardsStart}}'
AND start_timestamp <= TIMESTAMP '{{.cutoffDate}}'
AND block_time <= TIMESTAMP '{{.cutoffDate}}' -- 始终确保我们没有使用未来的数据。 由于我们从不回填,所以永远不会发生这种情况,但为了安全和一致性,这里加上。
),
-- 步骤 2:切割每个 reward 的开始和结束窗口以处理全局范围
active_rewards_updated_end_timestamps AS (
SELECT
avs,
operator_set_id,
operator,
/**
* 切割开始和结束窗口以处理
* A. 最近出现的追溯奖励,其开始日期小于 start_timestamp
* B. 此运行的任何奖励都不要超过 end_timestamp
*/
start_timestamp AS reward_start_exclusive,
LEAST(global_end_inclusive, end_timestamp) AS reward_end_inclusive,
amount,
token,
multiplier,
strategy,
reward_hash,
duration,
global_end_inclusive,
block_date AS reward_submission_date
FROM active_rewards_modified
),
-- 步骤 3:对于每个 reward hash,找到最新的 snapshot
active_rewards_updated_start_timestamps AS (
SELECT
ap.avs,
ap.operator_set_id,
ap.operator,
COALESCE(MAX(g.snapshot), ap.reward_start_exclusive) AS reward_start_exclusive,
ap.reward_end_inclusive,
ap.token,
-- 我们使用 floor 以确保我们始终低估每天的总tokens
FLOOR(ap.amount) AS amount_decimal,
ap.multiplier,
ap.strategy,
ap.reward_hash,
ap.duration,
ap.global_end_inclusive,
ap.reward_submission_date
FROM active_rewards_updated_end_timestamps ap
LEFT JOIN gold_table g
ON g.reward_hash = ap.reward_hash
GROUP BY
ap.avs,
ap.operator_set_id,
ap.operator,
ap.reward_end_inclusive,
ap.token,
ap.amount,
ap.multiplier,
ap.strategy,
ap.reward_hash,
ap.duration,
ap.global_end_inclusive,
ap.reward_start_exclusive,
ap.reward_submission_date
),
-- 步骤 4:过滤掉无效的 reward 范围
active_reward_ranges AS (
/** 取出 (reward_start_exclusive, reward_end_inclusive) 窗口,其中
* 1. reward_start_exclusive >= reward_end_inclusive:奖励期已完成,或者我们将在后续运行中处理
*/
SELECT *
FROM active_rewards_updated_start_timestamps
WHERE reward_start_exclusive < reward_end_inclusive
),
-- 步骤 5:展开范围,包括每个日期的天数
exploded_active_range_rewards AS (
SELECT
*
FROM active_reward_ranges
CROSS JOIN generate_series(
DATE(reward_start_exclusive),
DATE(reward_end_inclusive),
INTERVAL '1' DAY
) AS day
),
-- 步骤 6:准备好清理的 active rewards
active_rewards_cleaned AS (
SELECT
avs,
operator_set_id,
operator,
CAST(day AS DATE) AS snapshot,
token,
amount_decimal,
multiplier,
strategy,
duration,
reward_hash,
reward_submission_date
FROM exploded_active_range_rewards
-- 删除开始当天的快照
WHERE day != reward_start_exclusive
),
-- 步骤 7:通过 (avs, operator_set_id, snapshot, operator, reward_hash) 删除 active rewards 的重复数据
active_rewards_reduced_deduped AS (
SELECT DISTINCT avs, operator_set_id, snapshot, operator, reward_hash
FROM active_rewards_cleaned
),
-- 步骤 8:除以 operator 注册到 operator set 的快照数量
op_set_op_num_registered_snapshots AS (
SELECT
ar.reward_hash,
ar.operator,
COUNT(*) AS num_registered_snapshots
FROM active_rewards_reduced_deduped ar
JOIN operator_set_operator_registration_snapshots osor
ON
ar.avs = osor.avs
AND ar.operator_set_id = osor.operator_set_id
AND ar.snapshot = osor.snapshot
AND ar.operator = osor.operator
GROUP BY ar.reward_hash, ar.operator
),
-- 步骤 9:将 amount 除以 operator 注册的快照数量
active_rewards_with_registered_snapshots AS (
SELECT
arc.*,
COALESCE(nrs.num_registered_snapshots, 0) as num_registered_snapshots
FROM active_rewards_cleaned arc
LEFT JOIN op_set_op_num_registered_snapshots nrs
ON
arc.reward_hash = nrs.reward_hash
AND arc.operator = nrs.operator
),
-- 步骤 10:将 amount 除以 operator 注册的快照数量
active_rewards_final AS (
SELECT
ar.*,
CASE
-- 如果 operator 没有注册任何快照,只需获得每天的常规tokens来退还 AVS
WHEN ar.num_registered_snapshots = 0 THEN floor(ar.amount_decimal / (duration / 86400))
ELSE floor(ar.amount_decimal / ar.num_registered_snapshots)
END AS tokens_per_registered_snapshot_decimal
FROM active_rewards_with_registered_snapshots ar
)
SELECT * FROM active_rewards_final
-- 步骤 1:获取 operator 已注册 operator set 的行
WITH reward_snapshot_operators AS (
SELECT
ap.reward_hash,
ap.snapshot AS snapshot,
ap.token,
ap.tokens_per_registered_snapshot_decimal,
ap.avs AS avs,
ap.operator_set_id AS operator_set_id,
ap.operator AS operator,
ap.strategy,
ap.multiplier,
ap.reward_submission_date
FROM {{.activeODRewardsTable}} ap
JOIN operator_set_operator_registration_snapshots osor
ON ap.avs = osor.avs
AND ap.operator_set_id = osor.operator_set_id
AND ap.snapshot = osor.snapshot
AND ap.operator = osor.operator
),
-- 步骤 2:删除每个 (operator, reward hash, snapshot) 中跨 strategies 的 operator tokens 的重复数据 -- 由于以上结果是一个扁平化的 operator-directed reward 提交
distinct_operators AS (
SELECT *
FROM (
SELECT
*,
-- 这里我们可以使用任意顺序,因为每个 (operator, strategy, hash, snapshot) 的 avs_tokens 都是相同的
-- 我们使用 strategy ASC 以便更好地进行调试
ROW_NUMBER() OVER (
PARTITION BY reward_hash, snapshot, operator
ORDER BY strategy ASC
) AS rn
FROM reward_snapshot_operators
) t
-- 仅保留每个 (operator, reward hash, snapshot) 的第一行
WHERE rn = 1
),
-- 步骤 3:使用动态分割逻辑计算每个 operator 的 tokens -- 如果未找到分割,则默认为 1000 (10%)
operator_splits AS (
SELECT
dop.*,
COALESCE(oss.split, dos.split, 1000) / CAST(10000 AS DECIMAL) AS split_pct,
FLOOR(dop.tokens_per_registered_snapshot_decimal * COALESCE(oss.split, dos.split, 1000) / CAST(10000 AS DECIMAL)) AS operator_tokens
FROM distinct_operators dop
LEFT JOIN operator_set_split_snapshots oss
ON dop.operator = oss.operator
AND dop.avs = oss.avs
AND dop.operator_set_id = oss.operator_set_id
AND dop.snapshot = oss.snapshot
LEFT JOIN default_operator_split_snapshots dos ON (dop.snapshot = dos.snapshot)
)
-- 步骤 4:输出具有 operator 分割的最终表
SELECT * FROM operator_splits
-- 步骤 1:获取 operator 已注册 operator set 的行
WITH reward_snapshot_operators AS (
SELECT
ap.reward_hash,
ap.snapshot AS snapshot,
ap.token,
ap.tokens_per_registered_snapshot_decimal,
ap.avs AS avs,
ap.operator_set_id AS operator_set_id,
ap.operator AS operator,
ap.strategy,
ap.multiplier,
ap.reward_submission_date
FROM {{.activeODRewardsTable}} ap
JOIN operator_set_operator_registration_snapshots osor
ON ap.avs = osor.avs
AND ap.operator_set_id = osor.operator_set_id
AND ap.snapshot = osor.snapshot
AND ap.operator = osor.operator
),
-- 获取 strategies 已注册 operator set 的行
operator_set_strategy_registrations AS (
SELECT
rso.*
FROM reward_snapshot_operators rso
JOIN operator_set_strategy_registration_snapshots ossr
ON rso.avs = ossr.avs
AND rso.operator_set_id = ossr.operator_set_id
AND rso.snapshot = ossr.snapshot
AND rso.strategy = ossr.strategy
),
-- 使用动态分割逻辑计算每个 operator reward 的总 staker 分割 -- 如果未找到分割,则默认为 1000 (10%)
staker_splits AS (
SELECT
ossr.*,
ossr.tokens_per_registered_snapshot_decimal - FLOOR(ossr.tokens_per_registered_snapshot_decimal * COALESCE(oss.split, dos.split, 1000) / CAST(10000 AS DECIMAL)) AS staker_split
FROM operator_set_strategy_registrations ossr
LEFT JOIN operator_set_split_snapshots oss
ON ossr.operator = oss.operator
AND ossr.avs = oss.avs
AND ossr.operator_set_id = oss.operator_set_id
AND ossr.snapshot = oss.snapshot
LEFT JOIN default_operator_split_snapshots dos ON (ossr.snapshot = dos.snapshot)
),
-- 获取为 snapshot 委托给 operator 的 staker
staker_delegated_operators AS (
SELECT
ors.*,
sds.staker
FROM staker_splits ors
JOIN staker_delegation_snapshots sds
ON ors.operator = sds.operator
AND ors.snapshot = sds.snapshot
),
-- 获取委托给 operator 的 staker 的 shares
staker_strategy_shares AS (
SELECT
sdo.*,
sss.shares
FROM staker_delegated_operators sdo
JOIN staker_share_snapshots sss
ON sdo.staker = sss.staker
AND sdo.snapshot = sss.snapshot
AND sdo.strategy = sss.strategy
-- 过滤掉负 shares 和零 multiplier,以避免除以零
WHERE sss.shares > 0 AND sdo.multiplier != 0
),
-- 计算每个 staker 的权重
staker_weights AS (
SELECT
*,
SUM(multiplier * shares) OVER (PARTITION BY staker, reward_hash, snapshot) AS staker_weight
FROM staker_strategy_shares
),
-- 获取不同的 staker,因为他们的权重已经计算出来了
distinct_stakers AS (
SELECT *
FROM (
SELECT
*,
-- 这里我们可以使用任意顺序,因为每个(staker、strategy、hash、snapshot)的 staker_weight 都是相同的
-- 我们使用 strategy ASC 以便更好地进行调试
ROW_NUMBER() OVER (
PARTITION BY reward_hash, snapshot, staker
ORDER BY strategy ASC
) AS rn
FROM staker_weights
) t
WHERE rn = 1
ORDER BY reward_hash, snapshot, staker
),
-- 计算每个 reward 和 snapshot 的所有 staker 权重的总和
staker_weight_sum AS (
SELECT
*,
SUM(staker_weight) OVER (PARTITION BY reward_hash, operator, snapshot) AS total_weight
FROM distinct_stakers
),
-- 计算每个 reward 和 snapshot 的 tokens 的 staker 比例
staker_proportion AS (
SELECT
*,
FLOOR((staker_weight / total_weight) * 1000000000000000) / 1000000000000000 AS staker_proportion
FROM staker_weight_sum
),
-- 计算 staker reward 金额
staker_reward_amounts AS (
SELECT
*,
FLOOR(staker_proportion * staker_split) AS staker_tokens
FROM staker_proportion
)
-- 输出最终表
SELECT * FROM staker_reward_amounts
-- 步骤 1:获取 operator 尚未注册 AVS 或 AVS 不存在的行
WITH not_registered_operators AS (
SELECT
ap.reward_hash,
ap.snapshot AS snapshot,
ap.token,
ap.tokens_per_registered_snapshot_decimal,
ap.avs AS avs,
ap.operator_set_id AS operator_set_id,
ap.operator AS operator,
ap.strategy,
ap.multiplier,
ap.reward_submission_date
FROM {{.activeODRewardsTable}} ap
WHERE
ap.num_registered_snapshots = 0
),
-- 步骤 2:删除每个 (operator, reward hash, snapshot) 中跨 strategies 的 operator tokens 的重复数据 -- 由于以上结果是一个扁平化的 operator-directed reward 提交
distinct_not_registered_operators AS (
SELECT *
FROM (
SELECT
*,
-- 这里我们可以使用任意顺序,因为每个 (operator, strategy, hash, snapshot) 的 avs_tokens 都是相同的
-- 我们使用 strategy ASC 以便更好地进行调试
ROW_NUMBER() OVER (
PARTITION BY reward_hash, snapshot, operator
ORDER BY strategy ASC
) AS rn
FROM not_registered_operators
) t
WHERE rn = 1
),
-- 步骤 3:将每个(reward hash、snapshot)的 operator tokens 求和 -- 因为我们想要将这些 operator 金额的总和退还给该 snapshot 的 reward 提交中的 AVS
avs_operator_refund_sums AS (
SELECT
reward_hash,
snapshot,
token,
avs,
operator_set_id,
operator,
SUM(tokens_per_registered_snapshot_decimal) OVER (PARTITION BY reward_hash, snapshot) AS avs_tokens
FROM distinct_not_registered_operators
),
-- 步骤 4:查找 operator 已注册但 strategies 未注册到 operator set 的行 -- 首先,获取 operator 已注册的所有行
registered_operators AS (
SELECT
ap.reward_hash,
ap.snapshot,
ap.token,
ap.tokens_per_registered_snapshot_decimal,
ap.avs,
ap.operator_set_id,
ap.operator,
ap.strategy,
ap.multiplier,
ap.reward_submission_date
FROM {{.activeODRewardsTable}} ap
JOIN operator_set_operator_registration_snapshots osor
ON ap.avs = osor.avs
AND ap.operator_set_id = osor.operator_set_id
AND ap.snapshot = osor.snapshot
AND ap.operator = osor.operator
WHERE ap.num_registered_snapshots != 0
AND ap.reward_submission_date >= @coloradoHardforkDate
),
-- 步骤 5:对于每个 reward/snapshot/operator_set,检查是否已注册任何 strategies
strategies_registered AS (
SELECT DISTINCT
ro.reward_hash,
ro.snapshot,
ro.avs,
ro.operator_set_id
FROM registered_operators ro
JOIN operator_set_strategy_registration_snapshots ossr
ON ro.avs = ossr.avs
AND ro.operator_set_id = ossr.operator_set_id
AND ro.snapshot = ossr.snapshot
AND ro.strategy = ossr.strategy
),
-- 步骤 6:查找 operator 已注册但未注册 strategies 的 reward/snapshot 组合
strategies_not_registered AS (
SELECT
ro.*
FROM registered_operators ro
LEFT JOIN strategies_registered sr
ON ro.reward_hash = sr.reward_hash
AND ro.snapshot = sr.snapshot
AND ro.avs = sr.avs
AND ro.operator_set_id = sr.operator_set_id
WHERE sr.reward_hash IS NULL
),
-- 步骤 7:使用动态分割逻辑计算每个 reward 的 staker 分割 -- 如果未找到分割,则默认为 1000 (10%)
staker_splits AS (
SELECT
snr.*,
snr.tokens_per_registered_snapshot_decimal - FLOOR(snr.tokens_per_registered_snapshot_decimal * COALESCE(oss.split, dos.split, 1000) / CAST(10000 AS DECIMAL)) AS staker_split
FROM strategies_not_registered snr
LEFT JOIN operator_set_split_snapshots oss
ON snr.operator = oss.operator
AND snr.avs = oss.avs
AND snr.operator_set_id = oss.operator_set_id
AND snr.snapshot = oss.snapshot
LEFT JOIN default_operator_split_snapshots dos ON (snr.snapshot = dos.snapshot)
),
-- 步骤 8:删除每个(operator、reward hash、snapshot)中跨 strategies 的 staker 分割的重复数据 -- 由于以上结果是一个扁平化的 operator-directed reward 提交。
distinct_staker_splits AS (
SELECT *
FROM (
SELECT
*,
ROW_NUMBER() OVER (
PARTITION BY reward_hash, snapshot, operator
ORDER BY strategy ASC
) AS rn
FROM staker_splits
) t
WHERE rn = 1
),
-- 步骤 9:将应退还的每个(reward hash、snapshot)的 staker tokens 求和
avs_staker_refund_sums AS (
SELECT
reward_hash,
snapshot,
token,
avs,
operator_set_id,
operator,
SUM(staker_split) OVER (PARTITION BY reward_hash, snapshot) AS avs_tokens
FROM distinct_staker_splits
),
-- 步骤 10:将两个退款案例合并为一个结果
combined_avs_refund_amounts AS (
SELECT * FROM avs_operator_refund_sums
UNION ALL
SELECT * FROM avs_staker_refund_sums
)
-- 输出最终表
SELECT * FROM combined_avs_refund_amounts
此查询结合了步骤 2,3,4,5,6,7,8,9,10,11,12,13,14 中的 rewards,以生成具有以下列的表:
Earner(收益人) | Snapshot(快照) | Reward Hash(Reward哈希) | Token(Token) | Amount(金额) |
---|
汇总步骤 2、3 和 4 中的 rewards。 我们使用 DISTINCT
作为一个健全性检查,以丢弃给定 reward_hash
和 snapshot
的具有相同 strategy
和 earner
的行。
WITH staker_rewards AS (
-- 我们可以在这里选择 DISTINCT,因为 staker 的 tokens 对于 reward hash 中的每种 strategy 都是相同的
SELECT DISTINCT
staker as earner,
snapshot,
reward_hash,
token,
staker_tokens as amount
FROM {{.stakerRewardAmountsTable}}
),
operator_rewards AS (
SELECT DISTINCT
-- 我们可以在这里选择 DISTINCT,因为 operator 的 tokens 对于 reward hash 中的每种 strategy 都是相同的
operator as earner,
snapshot,
reward_hash,
token,
operator_tokens as amount
FROM {{.operatorRewardAmountsTable}}
),
rewards_for_all AS (
SELECT DISTINCT
staker as earner,
snapshot,
reward_hash,
token,
staker_tokens as amount
FROM {{.rewardsForAllTable}}
),
rewards_for_all_earners_stakers AS (
SELECT DISTINCT
staker as earner,
snapshot,
reward_hash,
token,
staker_tokens as amount
FROM {{.rfaeStakerTable}}
),
rewards_for_all_earners_operators AS (
SELECT DISTINCT
operator as earner,
snapshot,
reward_hash,
token,
operator_tokens as amount
FROM {{.rfaeOperatorTable}}
),
{{ if .enableRewardsV2 }}
operator_od_rewards AS (
SELECT DISTINCT
-- 我们可以在这里选择 DISTINCT,因为 operator 的 tokens 对于 reward hash 中的每种 strategy 都是相同的
operator as earner,
snapshot,
reward_hash,
token,
operator_tokens as amount
FROM {{.operatorODRewardAmountsTable}}
),
staker_od_rewards AS (
SELECT DISTINCT
-- 我们可以在这里选择 DISTINCT,因为 staker 的 tokens 对于 reward hash 中的每种 strategy 都是相同的
staker as earner,
snapshot,
reward_hash,
token,
staker_tokens as amount
FROM {{.stakerODRewardAmountsTable}}
),
avs_od_rewards AS (
SELECT DISTINCT
-- 我们可以在这里选择 DISTINCT,因为 avs 的 tokens 对于 reward hash 中的每种 strategy 都是相同的
avs as earner,
snapshot,
reward_hash,
token,
avs_tokens as amount
FROM {{.avsODRewardAmountsTable}}
),
{{ end }}
{{ if .enableRewardsV2_1 }}
operator_od_operator_set_rewards AS (
SELECT DISTINCT
-- 我们可以在这里选择 DISTINCT,因为 operator 的 tokens 对于 reward hash 中的每种 strategy 都是相同的
operator as earner,
snapshot,
reward_hash,
token,
operator_tokens as amount
FROM {{.operatorODOperatorSetRewardAmountsTable}}
),
staker_od_operator_set_rewards AS (
SELECT DISTINCT
-- 我们可以在这里选择 DISTINCT,因为 staker 的 tokens 对于 reward hash 中的每种 strategy 都是相同的
staker as earner,
snapshot,
reward_hash,
token,
staker_tokens as amount
FROM {{.stakerODOperatorSetRewardAmountsTable}}
),
avs_od_operator_set_rewards AS (
SELECT DISTINCT
-- 我们可以在这里选择 DISTINCT,因为 avs 的 tokens 对于 reward hash 中的每种 strategy 都是相同的
avs as earner,
snapshot,
reward_hash,
token,
avs_tokens as amount
FROM {{.avsODOperatorSetRewardAmountsTable}}
),
{{ end }}
combined_rewards AS (
SELECT * FROM operator_rewards
UNION ALL
SELECT * FROM staker_rewards
UNION ALL
SELECT * FROM rewards_for_all
UNION ALL
SELECT * FROM rewards_for_all_earners_stakers
UNION ALL
SELECT * FROM rewards_for_all_earners_operators
{{ if .enableRewardsV2 }}
UNION ALL
SELECT * FROM operator_od_rewards
UNION ALL
SELECT * FROM staker_od_rewards
UNION ALL
SELECT * FROM avs_od_rewards
{{ end }}
{{ if .enableRewardsV2_1 }}
UNION ALL
SELECT * FROM operator_od_operator_set_rewards
UNION ALL
SELECT * FROM staker_od_operator_set_rewards
UNION ALL
SELECT * FROM avs_od_operator_set_rewards
{{ end }}
)
-- 删除 earners 的重复数据,主要是作为他们自己的 staker 的 operators。
deduped_earners AS (
SELECT
earner,
snapshot,
reward_hash,
token,
SUM(amount) as amount
FROM combined_rewards
GROUP BY
earner,
snapshot,
reward_hash,
token
)
SELECT *
FROM deduped_earners
对给定 reward_hash
和 snapshot
的具有多行的 earners 的余额进行求和。 此步骤处理了 operators 也委托给自己的情况。
之前的查询都是视图。 此步骤从 步骤 7 中选择行,并将它们附加到表中。
SELECT
earner,
snapshot,
reward_hash,
token,
amount
FROM {{ ref('gold_staging') }}
此表通过 merge
incremental_strategy
在 ['reward_hash', 'earner', 'snapshot']
的唯一键上合并。 有关更多信息,请参见 dbt 文档。
最后,Rewards Root Updater 将查询此表以获取累积金额,merkelize 以下 earner, token, cumulative_amount 列。
SELECT
earner,
snapshot,
reward_hash,
token,
amount
FROM {{ ref('gold_staging') }}
- 原文链接: github.com/Layr-Labs/sid...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!