Skip to content

Commit

Permalink
carp funnel stake delegation cde epoch tracking (#265)
Browse files Browse the repository at this point in the history
* add epoch boundary tracking for cardano delegation primitive

* skip redundant epoch change events

* return two delegation entries only when current is current epoch

* carp funnel: add last epoch to the cache

* refactor the time computation config

* address review comments
  • Loading branch information
ecioppettini authored Jan 2, 2024
1 parent 270f9b8 commit 1ca150e
Show file tree
Hide file tree
Showing 16 changed files with 357 additions and 42 deletions.
11 changes: 8 additions & 3 deletions packages/engine/paima-funnel/src/cde/cardanoPool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export default async function getCdeData(
extension: ChainDataExtensionCardanoDelegation,
fromAbsoluteSlot: number,
toAbsoluteSlot: number,
getBlockNumber: (slot: number) => number
getBlockNumber: (slot: number) => number,
absoluteSlotToEpoch: (slot: number) => number
): Promise<ChainDataExtensionDatum[]> {
const events = await timeout(
query(url, Routes.delegationForPool, {
Expand All @@ -22,13 +23,16 @@ export default async function getCdeData(
DEFAULT_FUNNEL_TIMEOUT
);

return events.map(e => eventToCdeDatum(e, extension, getBlockNumber(e.slot)));
return events.map(e =>
eventToCdeDatum(e, extension, getBlockNumber(e.slot), absoluteSlotToEpoch(e.slot))
);
}

function eventToCdeDatum(
event: DelegationForPoolResponse[0],
extension: ChainDataExtensionCardanoDelegation,
blockNumber: number
blockNumber: number,
epoch: number
): CdeCardanoPoolDatum {
return {
cdeId: extension.cdeId,
Expand All @@ -37,6 +41,7 @@ function eventToCdeDatum(
payload: {
address: event.credential,
pool: event.pool || undefined,
epoch,
},
scheduledPrefix: extension.scheduledPrefix,
};
Expand Down
9 changes: 8 additions & 1 deletion packages/engine/paima-funnel/src/funnels/FunnelCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,15 @@ export class RpcCacheEntry implements FunnelCacheEntry {
export type CarpFunnelCacheEntryState = {
startingSlot: number;
lastPoint: { blockHeight: number; timestamp: number } | undefined;
epoch: number | undefined;
};

export class CarpFunnelCacheEntry implements FunnelCacheEntry {
private state: CarpFunnelCacheEntryState | null = null;
public static readonly SYMBOL = Symbol('CarpFunnelStartingSlot');

public updateStartingSlot(startingSlot: number): void {
this.state = { startingSlot, lastPoint: this.state?.lastPoint };
this.state = { startingSlot, lastPoint: this.state?.lastPoint, epoch: this.state?.epoch };
}

public updateLastPoint(blockHeight: number, timestamp: number): void {
Expand All @@ -92,6 +93,12 @@ export class CarpFunnelCacheEntry implements FunnelCacheEntry {
}
}

public updateEpoch(epoch: number): void {
if (this.state) {
this.state.epoch = epoch;
}
}

public initialized(): boolean {
return !!this.state;
}
Expand Down
109 changes: 89 additions & 20 deletions packages/engine/paima-funnel/src/funnels/carp/funnel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@ import {
Network,
timeout,
} from '@paima/utils';
import type { ChainDataExtensionCardanoProjectedNFT } from '@paima/sm';
import type { ChainDataExtensionCardanoProjectedNFT, InternalEvent } from '@paima/sm';
import {
type ChainData,
type ChainDataExtension,
type ChainDataExtensionCardanoDelegation,
type ChainDataExtensionDatum,
type PresyncChainData,
} from '@paima/sm';
Expand All @@ -25,26 +24,60 @@ import getCdePoolData from '../../cde/cardanoPool.js';
import getCdeProjectedNFTData from '../../cde/cardanoProjectedNFT.js';
import { query } from '@dcspark/carp-client/client/src/index';
import { Routes } from '@dcspark/carp-client/shared/routes';
import { FUNNEL_PRESYNC_FINISHED } from '@paima/utils/src/constants';
import { FUNNEL_PRESYNC_FINISHED, InternalEventType } from '@paima/utils/src/constants';
import { CarpFunnelCacheEntry } from '../FunnelCache.js';
import { getCardanoEpoch } from '@paima/db';

const delayForWaitingForFinalityLoop = 1000;

// This returns the unix timestamp of the first block in the Shelley era of the
// configured network, and the slot of the corresponding block.
function knownShelleyTime(): { timestamp: number; absoluteSlot: number } {
type Era = {
firstSlot: number;
startEpoch: number;
slotsPerEpoch: number;
timestamp: number;
};

function shelleyEra(): Era {
switch (ENV.CARDANO_NETWORK) {
case 'preview':
return { timestamp: 1666656000, absoluteSlot: 0 };
return {
firstSlot: 0,
startEpoch: 0,
slotsPerEpoch: 86400,
timestamp: 1666656000,
};
case 'preprod':
return { timestamp: 1655769600, absoluteSlot: 86400 };
return {
firstSlot: 86400,
startEpoch: 4,
slotsPerEpoch: 432000,
timestamp: 1655769600,
};
case 'mainnet':
return { timestamp: 1596059091, absoluteSlot: 4492800 };
return {
firstSlot: 4492800,
startEpoch: 208,
slotsPerEpoch: 432000,
timestamp: 1596059091,
};
default:
throw new Error('unknown cardano network');
}
}

function absoluteSlotToEpoch(era: Era, slot: number): number {
const slotRelativeToEra = slot - era.firstSlot;

if (slotRelativeToEra >= 0) {
return era.startEpoch + Math.floor(slotRelativeToEra / era.slotsPerEpoch);
} else {
// this shouldn't really happen in practice, unless for some reason the
// indexed EVM blocks are older than the start of the shelley era (which
// does not apply to the presync).
throw new Error('slot number is not in the current era');
}
}

/*
Maps an EVM timestamp to a unique absolute slot in Cardano.
Expand All @@ -60,14 +93,12 @@ Note: The state pairing only matters after the presync stage is done, so as
long as the timestamp of the block specified in START_BLOCKHEIGHT happens after
the first Shelley block, we don't need to consider the previous Cardano era (if any).
*/
function timestampToAbsoluteSlot(timestamp: number, confirmationDepth: number): number {
function timestampToAbsoluteSlot(era: Era, timestamp: number, confirmationDepth: number): number {
const cardanoAvgBlockPeriod = 20;
// map timestamps with a delta, since we are waiting for blocks.
const confirmationTimeDelta = cardanoAvgBlockPeriod * confirmationDepth;

const era = knownShelleyTime();

return timestamp - confirmationTimeDelta - era.timestamp + era.absoluteSlot;
return timestamp - confirmationTimeDelta - era.timestamp + era.firstSlot;
}

export class CarpFunnel extends BaseFunnel implements ChainFunnel {
Expand All @@ -85,9 +116,11 @@ export class CarpFunnel extends BaseFunnel implements ChainFunnel {
this.readPresyncData.bind(this);
this.getDbTx.bind(this);
this.bufferedData = null;
this.era = shelleyEra();
}

private bufferedData: ChainData[] | null;
private era: Era;

public override async readData(blockHeight: number): Promise<ChainData[]> {
if (!this.bufferedData || this.bufferedData[0].blockNumber != blockHeight) {
Expand Down Expand Up @@ -124,11 +157,37 @@ export class CarpFunnel extends BaseFunnel implements ChainFunnel {
this.sharedData.extensions,
lastTimestamp,
this.cache,
this.confirmationDepth
this.confirmationDepth,
this.era
);

const composed = composeChainData(this.bufferedData, grouped);

for (const data of composed) {
if (!data.internalEvents) {
data.internalEvents = [] as InternalEvent[];

const epoch = absoluteSlotToEpoch(
this.era,
timestampToAbsoluteSlot(this.era, data.timestamp, this.confirmationDepth)
);

const prevEpoch = this.cache.getState().epoch;

if (!prevEpoch || epoch !== prevEpoch) {
data.internalEvents.push({
type: InternalEventType.CardanoBestEpoch,
epoch: epoch,
});

// The execution of the event that we just pushed should set the
// `cardano_last_epoch` table to `epoch`. This cache entry mirrors the
// value of that table, so we need to update it here too.
this.cache.updateEpoch(epoch);
}
}
}

this.bufferedData = null;

return composed;
Expand Down Expand Up @@ -159,7 +218,8 @@ export class CarpFunnel extends BaseFunnel implements ChainFunnel {
Math.min(arg.to, this.cache.getState().startingSlot - 1),
slot => {
return slot;
}
},
slot => absoluteSlotToEpoch(this.era, slot)
);
return data;
} else {
Expand Down Expand Up @@ -220,11 +280,18 @@ export class CarpFunnel extends BaseFunnel implements ChainFunnel {

newEntry.updateStartingSlot(
timestampToAbsoluteSlot(
shelleyEra(),
(await sharedData.web3.eth.getBlock(startingBlockHeight)).timestamp as number,
confirmationDepth
)
);

const epoch = await getCardanoEpoch.run(undefined, dbTx);

if (epoch.length === 1) {
newEntry.updateEpoch(epoch[0].epoch);
}

return newEntry;
})();

Expand All @@ -245,14 +312,15 @@ async function readDataInternal(
extensions: ChainDataExtension[],
lastTimestamp: number,
cache: CarpFunnelCacheEntry,
confirmationDepth: number
confirmationDepth: number,
era: Era
): Promise<PresyncChainData[]> {
// the lower range is exclusive
const min = timestampToAbsoluteSlot(lastTimestamp, confirmationDepth);
const min = timestampToAbsoluteSlot(era, lastTimestamp, confirmationDepth);
// the upper range is inclusive
const maxElement = data[data.length - 1];

const max = timestampToAbsoluteSlot(maxElement.timestamp, confirmationDepth);
const max = timestampToAbsoluteSlot(era, maxElement.timestamp, confirmationDepth);

cache.updateLastPoint(maxElement.blockNumber, maxElement.timestamp);

Expand All @@ -277,7 +345,7 @@ async function readDataInternal(

const blockNumbers = data.reduce(
(dict, data) => {
dict[timestampToAbsoluteSlot(data.timestamp, confirmationDepth)] = data.blockNumber;
dict[timestampToAbsoluteSlot(era, data.timestamp, confirmationDepth)] = data.blockNumber;
return dict;
},
{} as { [slot: number]: number }
Expand Down Expand Up @@ -308,7 +376,8 @@ async function readDataInternal(
extension,
min,
Math.min(max, extension.stopSlot || max),
mapSlotToBlockNumber
mapSlotToBlockNumber,
slot => absoluteSlotToEpoch(era, slot)
);

return poolData;
Expand Down
15 changes: 13 additions & 2 deletions packages/engine/paima-sm/src/cde-cardano-pool.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ENV } from '@paima/utils';
import { createScheduledData, cdeCardanoPoolInsertData } from '@paima/db';
import { createScheduledData, cdeCardanoPoolInsertData, removeOldEntries } from '@paima/db';
import type { SQLUpdate } from '@paima/db';
import type { CdeCardanoPoolDatum } from './types.js';

Expand All @@ -16,7 +16,18 @@ export default async function processDatum(cdeDatum: CdeCardanoPoolDatum): Promi
createScheduledData(scheduledInputData, scheduledBlockHeight),
[
cdeCardanoPoolInsertData,
{ cde_id: cdeId, address: cdeDatum.payload.address, pool: cdeDatum.payload.pool },
{
cde_id: cdeId,
address: cdeDatum.payload.address,
pool: cdeDatum.payload.pool,
epoch: cdeDatum.payload.epoch,
},
],
[
removeOldEntries,
{
address: cdeDatum.payload.address,
},
],
];
return updateList;
Expand Down
22 changes: 22 additions & 0 deletions packages/engine/paima-sm/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ChainDataExtensionDatumType,
doLog,
ENV,
InternalEventType,
Network,
SCHEDULED_DATA_ADDRESS,
} from '@paima/utils';
Expand All @@ -24,6 +25,7 @@ import {
getLatestProcessedCdeBlockheight,
getCardanoLatestProcessedCdeSlot,
markCardanoCdeSlotProcessed,
updateCardanoEpoch,
} from '@paima/db';
import Prando from '@paima/prando';

Expand All @@ -36,6 +38,7 @@ import type {
ChainDataExtensionDatum,
GameStateTransitionFunction,
GameStateMachineInitializer,
InternalEvent,
} from './types.js';
export * from './types.js';
export type * from './types.js';
Expand Down Expand Up @@ -168,6 +171,8 @@ const SM: GameStateMachineInitializer = {
dbTx
);

await processInternalEvents(latestChainData.internalEvents, dbTx);

const checkpointName = `game_sm_start`;
await dbTx.query(`SAVEPOINT ${checkpointName}`);
try {
Expand Down Expand Up @@ -359,4 +364,21 @@ async function processUserInputs(
return latestChainData.submittedData.length;
}

async function processInternalEvents(
events: InternalEvent[] | undefined,
dbTx: PoolClient
): Promise<void> {
if (!events) {
return;
}

for (const event of events) {
switch (event.type) {
case InternalEventType.CardanoBestEpoch:
await updateCardanoEpoch.run({ epoch: event.epoch }, dbTx);
break;
}
}
}

export default SM;
6 changes: 6 additions & 0 deletions packages/engine/paima-sm/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
OldERC6551RegistryContract,
ERC6551RegistryContract,
Network,
InternalEventType,
} from '@paima/utils';
import { Type } from '@sinclair/typebox';
import type { Static } from '@sinclair/typebox';
Expand All @@ -25,8 +26,12 @@ export interface ChainData {
blockNumber: number;
submittedData: SubmittedData[];
extensionDatums?: ChainDataExtensionDatum[];
internalEvents?: InternalEvent[];
}

export type InternalEvent = CardanoEpochEvent;
export type CardanoEpochEvent = { type: InternalEventType.CardanoBestEpoch; epoch: number };

export interface PresyncChainData {
network: Network;
blockNumber: number;
Expand Down Expand Up @@ -69,6 +74,7 @@ interface CdeDatumErc6551RegistryPayload {
interface CdeDatumCardanoPoolPayload {
address: string;
pool: string | undefined;
epoch: number;
}

interface CdeDatumCardanoProjectedNFTPayload {
Expand Down
Loading

0 comments on commit 1ca150e

Please sign in to comment.