Skip to content

Commit

Permalink
Isolate STF failures to the single input that caused them
Browse files Browse the repository at this point in the history
  • Loading branch information
SebastienGllmt committed May 24, 2024
1 parent df30db4 commit ae91c66
Showing 1 changed file with 73 additions and 51 deletions.
124 changes: 73 additions & 51 deletions packages/engine/paima-sm/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
updateCardanoEpoch,
updateCardanoPaginationCursor,
} from '@paima/db';
import type { SQLUpdate } from '@paima/db';
import Prando from '@paima/prando';

import { randomnessRouter } from './randomness.js';
Expand Down Expand Up @@ -211,44 +212,56 @@ const SM: GameStateMachineInitializer = {

await processInternalEvents(latestChainData.internalEvents, dbTx);

const checkpointName = `game_sm_start`;
await dbTx.query(`SAVEPOINT ${checkpointName}`);
try {
// Fetch and execute scheduled input data
const scheduledInputsLength = await processScheduledData(
latestChainData,
dbTx,
gameStateTransition,
randomnessGenerator
);
// Fetch and execute scheduled input data
const scheduledInputsLength = await processScheduledData(
latestChainData,
dbTx,
gameStateTransition,
randomnessGenerator
);

// Execute user submitted input data
const userInputsLength = await processUserInputs(
latestChainData,
dbTx,
gameStateTransition,
randomnessGenerator
// Execute user submitted input data
const userInputsLength = await processUserInputs(
latestChainData,
dbTx,
gameStateTransition,
randomnessGenerator
);

// Extra logging
if (cdeDataLength + userInputsLength + scheduledInputsLength > 0)
doLog(
`Processed ${userInputsLength} user inputs, ${scheduledInputsLength} scheduled inputs and ${cdeDataLength} CDE events in block #${latestChainData.blockNumber}`
);

// Extra logging
if (cdeDataLength + userInputsLength + scheduledInputsLength > 0)
doLog(
`Processed ${userInputsLength} user inputs, ${scheduledInputsLength} scheduled inputs and ${cdeDataLength} CDE events in block #${latestChainData.blockNumber}`
);
} catch (e) {
await dbTx.query(`ROLLBACK TO SAVEPOINT ${checkpointName}`);
throw e;
} finally {
await dbTx.query(`RELEASE SAVEPOINT ${checkpointName}`);

// Commit finishing of processing to DB
await blockHeightDone.run({ block_height: latestChainData.blockNumber }, dbTx);
}
// Commit finishing of processing to DB
await blockHeightDone.run({ block_height: latestChainData.blockNumber }, dbTx);
},
};
},
};

/**
* We need to process all the SQL calls of an STF update in an all-or-nothing manner
* STF updates can fail (since the data for them comes from arbitrary onchain data)
* But we can't allow a single user's bad transaction to DOS the game for everybody else
* So failures should be isolated to just the specific input, and not the full block
* (recall: without this, in psql, if a query fails during a db transaction, the entire dbTx becomes invalid)
*/
async function tryOrRollback<T>(dbTx: PoolClient, func: () => Promise<T>): Promise<undefined | T> {
const checkpointName = `game_state_transition`;
await dbTx.query(`SAVEPOINT ${checkpointName}`);
try {
return await func();
} catch (err) {
await dbTx.query(`ROLLBACK TO SAVEPOINT ${checkpointName}`);
doLog(`[paima-sm] Database error on ${checkpointName}. Rolling back.`, err);
return undefined;
} finally {
await dbTx.query(`RELEASE SAVEPOINT ${checkpointName}`);
}
}

async function processCdeDataBase(
cdeData: ChainDataExtensionDatum[] | undefined,
dbTx: PoolClient,
Expand Down Expand Up @@ -362,21 +375,26 @@ async function processScheduledData(
scheduled: true,
};
// Trigger STF
const sqlQueries = await gameStateTransition(
inputData,
data.block_height,
randomnessGenerator,
DBConn
);
let sqlQueries: SQLUpdate[] = [];
try {
sqlQueries = await gameStateTransition(
inputData,
data.block_height,
randomnessGenerator,
DBConn
);
} catch (err) {
// skip scheduled data where the STF fails
doLog(`[paima-sm] Error on scheduled data STF call. Skipping`, err);
continue;
}

await tryOrRollback(DBConn, async () => {
for (const [query, params] of sqlQueries) {
await query.run(params, DBConn);
}
await deleteScheduled.run({ id: data.id }, DBConn);
} catch (err) {
doLog(`[paima-sm] Database error on deleteScheduled: ${err}`);
throw err;
}
});
}
return scheduledData.length;
}
Expand Down Expand Up @@ -430,14 +448,21 @@ async function processUserInputs(
}

// Trigger STF
const sqlQueries = await gameStateTransition(
inputData,
latestChainData.blockNumber,
randomnessGenerator,
DBConn
);

let sqlQueries: SQLUpdate[] = [];
try {
sqlQueries = await gameStateTransition(
inputData,
latestChainData.blockNumber,
randomnessGenerator,
DBConn
);
} catch (err) {
// skip inputs where the STF fails
doLog(`[paima-sm] Error on user input STF call. Skipping`, err);
continue;
}

await tryOrRollback(DBConn, async () => {
for (const [query, params] of sqlQueries) {
await query.run(params, DBConn);
}
Expand All @@ -458,10 +483,7 @@ async function processUserInputs(
DBConn
);
}
} catch (err) {
doLog(`[paima-sm] Database error on gameStateTransition: ${err}`);
throw err;
}
});
}
return latestChainData.submittedData.length;
}
Expand Down

0 comments on commit ae91c66

Please sign in to comment.