Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

carp funnel stake delegation cde epoch tracking #265

Merged
Merged
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
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