Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/indexer-agent/src/__tests__/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,7 @@ describe('reconcileDeploymentAllocationAction', () => {
expect.anything(),
[activeAllocations[0]],
network,
10,
)
})

Expand Down
59 changes: 59 additions & 0 deletions packages/indexer-agent/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import {
timer,
} from '@graphprotocol/common-ts'
import {
ActionManager,
ActionStatus,
ActionType,
Allocation,
AllocationManagementMode,
allocationRewardsPool,
Expand All @@ -20,6 +22,7 @@ import {
IndexingRuleAttributes,
Network,
POIDisputeAttributes,
presentPOIReasonEpoch,
RewardsPool,
Subgraph,
SubgraphDeployment,
Expand Down Expand Up @@ -1023,9 +1026,64 @@ export class Agent {
}
},
)

// Rewards accrue per block, so a second present-POI the same epoch collects only
// the little earned since the last and mostly wastes gas. Skip allocations already
// harvested this epoch.
const harvestedThisEpoch = await this.allocationsHarvestedInEpoch(
deploymentAllocationDecision.deployment.ipfsHash,
network.specification.networkIdentifier,
epoch,
)
expiredAllocations = expiredAllocations.filter(allocation => {
if (harvestedThisEpoch.has(allocation.id)) {
logger.debug(
'Allocation already harvested this epoch, skipping presentPOI',
{
allocation: allocation.id,
epoch,
},
)
return false
}
return true
})

return expiredAllocations
}

// Returns the set of allocation ids that already had a successful present-POI
// scheduled for the current epoch, read from the actions table.
async allocationsHarvestedInEpoch(
deploymentID: string,
protocolNetwork: string,
epoch: number,
): Promise<Set<string>> {
const actionManager = this.indexerManagement.actionManager
if (!actionManager) {
return new Set()
}
const recentPresentPOIActions = await ActionManager.fetchActions(
actionManager.models,
null,
{
type: ActionType.PRESENT_POI,
status: ActionStatus.SUCCESS,
protocolNetwork,
},
)
return new Set(
recentPresentPOIActions
.filter(
action =>
action.deploymentID === deploymentID &&
action.allocationID !== null &&
presentPOIReasonEpoch(action.reason) === epoch,
)
.map(action => action.allocationID as string),
)
}

async reconcileDeploymentAllocationAction(
deploymentAllocationDecision: AllocationDecision,
activeAllocations: Allocation[],
Expand Down Expand Up @@ -1125,6 +1183,7 @@ export class Agent {
logger,
expiringAllocations,
network,
epoch,
)
}
}
Expand Down
22 changes: 21 additions & 1 deletion packages/indexer-common/src/__tests__/actions.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { ActionInput, ActionStatus, ActionType, isValidActionInput } from '../actions'
import {
ActionInput,
ActionStatus,
ActionType,
isValidActionInput,
presentPOIReason,
presentPOIReasonEpoch,
} from '../actions'

describe('Action Validation', () => {
describe('isValidActionInput', () => {
Expand Down Expand Up @@ -246,3 +253,16 @@ describe('Action Validation', () => {
})
})
})

describe('present-POI reason encoding', () => {
test('round-trips the epoch through the reason string', () => {
expect(presentPOIReasonEpoch(presentPOIReason(853))).toBe(853)
expect(presentPOIReasonEpoch(presentPOIReason(0))).toBe(0)
})

test('returns undefined for a reason without an epoch', () => {
expect(presentPOIReasonEpoch('presentPOI:staleness-prevention')).toBeUndefined()
expect(presentPOIReasonEpoch('some-other-reason')).toBeUndefined()
expect(presentPOIReasonEpoch(null)).toBeUndefined()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ describe('presentPOIForAllocations', () => {
id: '0x0000000000000000000000000000000000000002',
})

await operator.presentPOIForAllocations(mockLogger, [alloc1, alloc2], network)
await operator.presentPOIForAllocations(mockLogger, [alloc1, alloc2], network, 853)

expect(queueActionSpy).toHaveBeenCalledTimes(2)
expect(queueActionSpy).toHaveBeenCalledWith(
Expand All @@ -71,7 +71,7 @@ describe('presentPOIForAllocations', () => {
allocationID: alloc1.id,
deploymentID: deployment.ipfsHash,
}),
reason: 'presentPOI:staleness-prevention',
reason: 'presentPOI:staleness-prevention:epoch=853',
protocolNetwork: 'eip155:421614',
}),
false,
Expand All @@ -87,7 +87,7 @@ describe('presentPOIForAllocations', () => {
})

it('should not queue anything when no allocations are passed', async () => {
await operator.presentPOIForAllocations(mockLogger, [], network)
await operator.presentPOIForAllocations(mockLogger, [], network, 853)
expect(queueActionSpy).not.toHaveBeenCalled()
})
})
16 changes: 16 additions & 0 deletions packages/indexer-common/src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,22 @@ export enum ActionType {
RESIZE = 'resize',
}

// Rewards accrue per block, so a present-POI mid-epoch collects only what accrued
// since the last and mostly wastes gas; one per epoch suffices. We record the epoch
// in the action `reason` so the agent can skip allocations already harvested this epoch.
const PRESENT_POI_REASON_PREFIX = 'presentPOI:staleness-prevention'

export function presentPOIReason(epoch: number): string {
return `${PRESENT_POI_REASON_PREFIX}:epoch=${epoch}`
}

// Returns the epoch encoded in a present-POI action `reason`, or undefined if
// the reason carries no epoch (e.g. an action queued before this field existed).
export function presentPOIReasonEpoch(reason: string | null): number | undefined {
const match = reason?.match(/:epoch=(\d+)$/)
return match ? Number(match[1]) : undefined
}

export enum ActionStatus {
QUEUED = 'queued',
APPROVED = 'approved',
Expand Down
5 changes: 4 additions & 1 deletion packages/indexer-common/src/operator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
Action,
POIDisputeAttributes,
DipsManager,
presentPOIReason,
} from '@graphprotocol/indexer-common'
import { Logger, formatGRT } from '@graphprotocol/common-ts'
import { hexlify } from 'ethers'
Expand Down Expand Up @@ -569,11 +570,13 @@ export class Operator {
network: {
specification: { networkIdentifier: string }
},
epoch: number,
): Promise<void> {
for (const allocation of expiringAllocations) {
logger.info('Scheduling presentPOI for Horizon allocation', {
allocationId: allocation.id,
deployment: allocation.subgraphDeployment.id.ipfsHash,
epoch,
})

await this.queueAction(
Expand All @@ -584,7 +587,7 @@ export class Operator {
poi: undefined,
},
type: ActionType.PRESENT_POI,
reason: 'presentPOI:staleness-prevention',
reason: presentPOIReason(epoch),
protocolNetwork: network.specification.networkIdentifier,
},
false,
Expand Down
Loading