Skip to content

Commit 29bb349

Browse files
oten91claude
andcommitted
feat: index v0.1.34 settlement events
Support the consensus-breaking event-payload additions shipped in poktroll v0.1.34 (pokt-network/poktroll#1952). - New EventValidatorRewardDistribution entity + handler. Emitted once per bonded validator per op_reason per settlement block, it carries the canonical per-validator commission/delegator split. Build "VALIDATOR vs DELEGATOR" reward totals from this event rather than EventSettlementBatch: when a pokt address is both a validator account and a delegator on another validator, the bank-batch stream keys on (recipient, op_reason) and over-counts the validator side. Registered in EventHandlers and dispatched in indexRelays. - Prefer chain-emitted num_estimated_relays (field 13) in the EventClaimCreated and EventClaimExpired paths via _computeNumEstimatedRelays. Removes the divide-by-zero the raw CUPR math hits when num_relays == 0; falls back to deriving from compute units for pre-upgrade blocks. - Prefer chain-emitted supplier_stake_after_slash (EventSupplierSlashed field 9) for afterStakeAmount. Falls back to tracked stake minus penalty for pre-upgrade events; also robust when tracked stake drifts on pruned nodes. All additions are backward-compatible: new fields are nullable/optional and the handlers fall back to the prior derivation when a field is absent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 78c3d96 commit 29bb349

5 files changed

Lines changed: 171 additions & 6 deletions

File tree

schema.graphql

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1131,6 +1131,52 @@ type EventProofValidityChecked @entity {
11311131
event: Event!
11321132
}
11331133

1134+
# EventValidatorRewardDistribution is emitted once per bonded validator per
1135+
# settlement op_reason per settlement block (v0.1.34+). It summarizes how that
1136+
# validator's slice of the proposer reward pool was split into commission and
1137+
# (self / external) delegator rewards. It is bounded by the validator-set size,
1138+
# so it does NOT scale with delegators or claims (preserves the #1758 event-count
1139+
# reduction).
1140+
#
1141+
# ACCOUNTING NOTE: build "VALIDATOR vs DELEGATOR" reward totals from THIS event
1142+
# (per-validator) rather than from EventSettlementBatch / ModToAcctTransfer. When
1143+
# the same pokt address is both a validator account AND a delegator on another
1144+
# validator, the bank-batch stream buckets both incomes under the validator-side
1145+
# op_reason, over-counting VALIDATOR and under-counting DELEGATOR. The split here
1146+
# is unambiguous: commissionAmount is validator income; self/external delegator
1147+
# amounts are delegator-side.
1148+
#
1149+
# All *Amount fields are denominated in upokt (chain emits bare integer strings).
1150+
type EventValidatorRewardDistribution @entity {
1151+
id: ID!
1152+
# session_end_block_height of the FIRST claim in the batch. On the common
1153+
# single-session path this differs from the settlement block height only by the
1154+
# claim/proof window offsets; for a canonical timestamp use block.
1155+
sessionEndBlockHeight: BigInt!
1156+
# Distinguishes the Mint=Burn pool from the Global Mint pool.
1157+
opReason: SettlementOpReason!
1158+
# valoper... operator address.
1159+
validatorOperatorAddress: String! @index
1160+
# pokt... account address; commission + self-delegation slice are paid here.
1161+
validatorAccountAddress: String! @index
1162+
# Commission rate as a decimal string, e.g. "0.100000000000000000".
1163+
commissionRate: String!
1164+
# This validator's slice of the total proposer reward pool (upokt).
1165+
poolShareAmount: BigInt!
1166+
# Commission carved out of poolShareAmount, paid to the validator account (upokt).
1167+
commissionAmount: BigInt!
1168+
# Validator self-delegation slice of the post-commission remainder (upokt).
1169+
selfDelegationRewardAmount: BigInt!
1170+
# Portion of the post-commission remainder paid to external delegators (upokt).
1171+
delegatorsRewardAmount: BigInt!
1172+
# Total delegated stake backing this validator incl. self-delegation (upokt);
1173+
# the denominator for per-delegator reward attribution.
1174+
totalDelegatedStakeAmount: BigInt!
1175+
# Number of external (non-self) delegators that received a reward.
1176+
numDelegators: Int!
1177+
block: Block!
1178+
}
1179+
11341180
type Supply @entity {
11351181
id: ID!
11361182
denom: String! @index

src/mappings/handlers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
handleEventProofUpdated,
3232
handleEventProofValidityChecked,
3333
handleEventSettlementBatch,
34+
handleEventValidatorRewardDistribution,
3435
handleMsgCreateClaim,
3536
handleMsgSubmitProof,
3637
} from "./pocket/relays";
@@ -126,6 +127,7 @@ export const EventHandlers: Record<string, (events: Array<CosmosEvent>) => Promi
126127
"pocket.tokenomics.EventApplicationOverserviced": handleEventApplicationOverserviced,
127128
"pocket.tokenomics.EventApplicationReimbursementRequest": handleEventApplicationReimbursementRequest,
128129
"pocket.tokenomics.EventSettlementBatch": handleEventSettlementBatch,
130+
"pocket.tokenomics.EventValidatorRewardDistribution": handleEventValidatorRewardDistribution,
129131
"pocket.proof.EventClaimUpdated": handleEventClaimUpdated,
130132
"pocket.proof.EventProofUpdated": handleEventProofUpdated,
131133
"pocket.proof.EventProofValidityChecked": handleEventProofValidityChecked,

src/mappings/indexer.manager.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ async function indexRelays(msgByType: MessageByType, eventByType: EventByType):
174174
"pocket.tokenomics.EventApplicationOverserviced",
175175
"pocket.tokenomics.EventApplicationReimbursementRequest",
176176
"pocket.tokenomics.EventSettlementBatch",
177+
"pocket.tokenomics.EventValidatorRewardDistribution",
177178
];
178179

179180
await Promise.all([

src/mappings/pocket/relays.ts

Lines changed: 111 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { EventClaimSettledProps } from "../../types/models/EventClaimSettled";
2020
import { EventClaimUpdatedProps } from "../../types/models/EventClaimUpdated";
2121
import { EventProofUpdatedProps } from "../../types/models/EventProofUpdated";
2222
import { EventProofValidityCheckedProps } from "../../types/models/EventProofValidityChecked";
23+
import { EventValidatorRewardDistributionProps } from "../../types/models/EventValidatorRewardDistribution";
2324
import { MsgCreateClaimProps } from "../../types/models/MsgCreateClaim";
2425
import { MsgSubmitProofProps } from "../../types/models/MsgSubmitProof";
2526
import { CoinSDKType } from "../../types/proto-interfaces/cosmos/base/v1beta1/coin";
@@ -232,7 +233,8 @@ export function getAttributes(attributes: CosmosEvent["event"]["attributes"]) {
232233
chainNumEstimatedRelays: bigint | undefined,
233234
mintedUpokt: CoinSDKType | undefined,
234235
overservicingLossUpokt: CoinSDKType | undefined,
235-
deflationLossUpokt: CoinSDKType | undefined;
236+
deflationLossUpokt: CoinSDKType | undefined,
237+
supplierStakeAfterSlash: CoinSDKType | undefined;
236238

237239
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
238240
// @ts-ignore
@@ -341,6 +343,11 @@ export function getAttributes(attributes: CosmosEvent["event"]["attributes"]) {
341343
proofMissingPenalty = getDenomAndAmount(attribute.value as string);
342344
}
343345

346+
// EventSupplierSlashed field 9 (v0.1.34+): post-slash stake as a coin string.
347+
if (attribute.key === "supplier_stake_after_slash") {
348+
supplierStakeAfterSlash = getDenomAndAmount(attribute.value as string);
349+
}
350+
344351
if (attribute.key === "failure_reason") {
345352
failureReason = parseAttribute(attribute.value as string);
346353
}
@@ -414,6 +421,7 @@ export function getAttributes(attributes: CosmosEvent["event"]["attributes"]) {
414421
mintedUpokt,
415422
overservicingLossUpokt,
416423
deflationLossUpokt,
424+
supplierStakeAfterSlash,
417425
};
418426
}
419427

@@ -520,14 +528,20 @@ function _handleMsgCreateClaim(msg: CosmosMessage<MsgCreateClaim>): MsgCreateCla
520528
*/
521529

522530
const {
531+
chainNumEstimatedRelays,
523532
claimed,
524533
numClaimedComputedUnits,
525534
numEstimatedComputedUnits,
526535
numRelays,
527536
} = getAttributes(eventClaimCreated.attributes);
528537

529-
const CUPR = Math.floor(Number(numClaimedComputedUnits) / Number(numRelays))
530-
const numEstimatedRelays = BigInt(Math.round(Number(numEstimatedComputedUnits) / CUPR))
538+
// Prefer the chain-emitted num_estimated_relays (EventClaimCreated field 13,
539+
// v0.1.34+); fall back to deriving it from compute units for pre-upgrade blocks.
540+
// The helper also avoids the divide-by-zero that the raw CUPR math hits when
541+
// numRelays == 0.
542+
const numEstimatedRelays = _computeNumEstimatedRelays(
543+
chainNumEstimatedRelays, numEstimatedComputedUnits, numClaimedComputedUnits, numRelays,
544+
);
531545

532546
return {
533547
id: messageId(msg),
@@ -963,6 +977,7 @@ function _handleEventClaimSettled(
963977

964978
function _handleEventClaimExpired(event: CosmosEvent): EventClaimExpiredProps {
965979
const {
980+
chainNumEstimatedRelays,
966981
claim,
967982
claimed,
968983
expirationReason,
@@ -973,8 +988,12 @@ function _handleEventClaimExpired(event: CosmosEvent): EventClaimExpiredProps {
973988

974989
const { proof_validation_status, session_header, supplier_operator_address } = claim;
975990

976-
const CUPR = Math.floor(Number(numClaimedComputedUnits) / Number(numRelays))
977-
const numEstimatedRelays = BigInt(Math.round(Number(numEstimatedComputedUnits) / CUPR))
991+
// Prefer the chain-emitted num_estimated_relays (EventClaimExpired field 13,
992+
// v0.1.34+); fall back to deriving it for pre-upgrade blocks (also avoids the
993+
// divide-by-zero in the raw CUPR math when numRelays == 0).
994+
const numEstimatedRelays = _computeNumEstimatedRelays(
995+
chainNumEstimatedRelays, numEstimatedComputedUnits, numClaimedComputedUnits, numRelays,
996+
);
978997

979998
return {
980999
supplierId: supplier_operator_address,
@@ -1576,3 +1595,90 @@ export async function handleEventSettlementBatch(events: Array<CosmosEvent>): Pr
15761595
]);
15771596
}
15781597
}
1598+
1599+
// EventValidatorRewardDistribution (v0.1.34+) is emitted once per bonded
1600+
// validator per settlement op_reason per settlement block. It carries the
1601+
// canonical per-validator commission/delegator split, which is the correct
1602+
// source for "VALIDATOR vs DELEGATOR" reward totals — see the entity comment in
1603+
// schema.graphql for the cross-delegation accounting caveat that makes summing
1604+
// from EventSettlementBatch alone over-count the validator side.
1605+
function _handleEventValidatorRewardDistribution(event: CosmosEvent): EventValidatorRewardDistributionProps {
1606+
let sessionEndBlockHeight = BigInt(0),
1607+
opReason = "",
1608+
validatorOperatorAddress = "",
1609+
validatorAccountAddress = "",
1610+
commissionRate = "",
1611+
poolShareAmount = BigInt(0),
1612+
commissionAmount = BigInt(0),
1613+
selfDelegationRewardAmount = BigInt(0),
1614+
delegatorsRewardAmount = BigInt(0),
1615+
totalDelegatedStakeAmount = BigInt(0),
1616+
numDelegators = 0;
1617+
1618+
for (const attribute of event.event.attributes) {
1619+
switch (attribute.key) {
1620+
case "session_end_block_height":
1621+
sessionEndBlockHeight = BigInt(parseAttribute(attribute.value));
1622+
break;
1623+
case "op_reason":
1624+
opReason = parseAttribute(attribute.value);
1625+
break;
1626+
case "validator_operator_address":
1627+
validatorOperatorAddress = parseAttribute(attribute.value);
1628+
break;
1629+
case "validator_account_address":
1630+
validatorAccountAddress = parseAttribute(attribute.value);
1631+
break;
1632+
case "commission_rate":
1633+
commissionRate = parseAttribute(attribute.value);
1634+
break;
1635+
// *_upokt fields are emitted as bare integer strings (no denom).
1636+
case "pool_share_upokt":
1637+
poolShareAmount = BigInt(parseAttribute(attribute.value));
1638+
break;
1639+
case "commission_upokt":
1640+
commissionAmount = BigInt(parseAttribute(attribute.value));
1641+
break;
1642+
case "self_delegation_reward_upokt":
1643+
selfDelegationRewardAmount = BigInt(parseAttribute(attribute.value));
1644+
break;
1645+
case "delegators_reward_upokt":
1646+
delegatorsRewardAmount = BigInt(parseAttribute(attribute.value));
1647+
break;
1648+
case "total_delegated_stake_upokt":
1649+
totalDelegatedStakeAmount = BigInt(parseAttribute(attribute.value));
1650+
break;
1651+
case "num_delegators":
1652+
numDelegators = Number(parseAttribute(attribute.value));
1653+
break;
1654+
default:
1655+
break;
1656+
}
1657+
}
1658+
1659+
return {
1660+
// getEventId includes the event index, so per-validator/op_reason emissions
1661+
// within the same settlement block get distinct ids.
1662+
id: getEventId(event),
1663+
blockId: getBlockId(event.block),
1664+
sessionEndBlockHeight,
1665+
opReason: getSettlementOpReasonFromSDK(opReason),
1666+
validatorOperatorAddress,
1667+
validatorAccountAddress,
1668+
commissionRate,
1669+
poolShareAmount,
1670+
commissionAmount,
1671+
selfDelegationRewardAmount,
1672+
delegatorsRewardAmount,
1673+
totalDelegatedStakeAmount,
1674+
numDelegators,
1675+
};
1676+
}
1677+
1678+
export async function handleEventValidatorRewardDistribution(events: Array<CosmosEvent>): Promise<void> {
1679+
await optimizedBulkCreate(
1680+
"EventValidatorRewardDistribution",
1681+
events.map(_handleEventValidatorRewardDistribution),
1682+
"block_id",
1683+
);
1684+
}

src/mappings/pocket/suppliers.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -633,13 +633,16 @@ function _getValuesOldEventSupplierSlashed(event: CosmosEvent) {
633633
session: "",
634634
sessionStartHeight: BigInt(0),
635635
sessionEndHeight: BigInt(0),
636+
// Not emitted before v0.1.34; afterStakeAmount is derived from stake - penalty.
637+
supplierStakeAfterSlash: undefined as CoinSDKType | undefined,
636638
}
637639
}
638640

639641
function _getValuesEventSupplierSlashed(event: CosmosEvent) {
640642
const {
641643
claim,
642644
proofMissingPenalty,
645+
supplierStakeAfterSlash,
643646
} = getAttributes(event.event.attributes);
644647

645648
if (!claim || !claim.session_header || Object.keys(claim).length === 0) {
@@ -656,6 +659,7 @@ function _getValuesEventSupplierSlashed(event: CosmosEvent) {
656659
sessionStartHeight: BigInt(claim.session_header.session_start_block_height || "0"),
657660
proofMissingPenalty,
658661
proofValidationStatus: getClaimProofStatusFromSDK(claim.proof_validation_status),
662+
supplierStakeAfterSlash,
659663
}
660664
}
661665

@@ -678,6 +682,7 @@ export function _handleEventSupplierSlashed(
678682
session,
679683
sessionEndHeight,
680684
sessionStartHeight,
685+
supplierStakeAfterSlash,
681686
} = _getValuesEventSupplierSlashed(event);
682687

683688
if (!operatorAddress) {
@@ -695,7 +700,12 @@ export function _handleEventSupplierSlashed(
695700
}
696701

697702
const previousStakeAmount = currentSupplier.stakeAmount.valueOf();
698-
const afterStakeAmount = currentSupplier.stakeAmount - BigInt(proofMissingPenalty.amount);
703+
// Prefer the chain-emitted post-slash stake (supplier_stake_after_slash, v0.1.34+);
704+
// fall back to deriving it from the indexer-tracked stake minus the penalty for
705+
// pre-upgrade events (also robust when tracked stake drifts on pruned nodes).
706+
const afterStakeAmount = supplierStakeAfterSlash
707+
? BigInt(supplierStakeAfterSlash.amount)
708+
: currentSupplier.stakeAmount - BigInt(proofMissingPenalty.amount);
699709

700710
return {
701711
supplier: {

0 commit comments

Comments
 (0)