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

feat: nightfall-node challenger #1351

Closed
wants to merge 5 commits into from
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
164 changes: 68 additions & 96 deletions cli/lib/nf3.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,17 @@ function createQueue(options) {
return queue;
}

// TODO when SDK is refactored such that these functions are split by user, proposer and challenger,
// then there will only be one queue here. The constructor does not need to initialise clientBaseUrl
// for proposer/liquidityProvider/challenger and optimistBaseUrl, optimistWsUrl for a user etc
const userQueue = createQueue({ autostart: true, concurrency: 1 });
const challengerQueue = createQueue({ autostart: true, concurrency: 1 });
const liquidityProviderQueue = createQueue({ autostart: true, concurrency: 1 });

/**
@class
Creates a new Nightfall_3 library instance.
@param {string} clientBaseUrl - The base url for nightfall-client
@param {string} optimistBaseUrl - The base url for nightfall-optimist
@param {string} optimistWsUrl - The webscocket url for nightfall-optimist
@param {string} optimistWsUrl - The websocket url for nightfall-optimist
@param {string} web3WsUrl - The websocket url for the web3js client
@param {string} ethereumSigningKey - the Ethereum siging key to be used for transactions (hex string).
@param {string} ethereumSigningKey - the Ethereum signing key to be used for transactions (hex string).
@param {object} zkpKeys - An object containing the zkp keys to use. These will be auto-generated if left undefined.
*/
class Nf3 {
Expand Down Expand Up @@ -415,7 +411,7 @@ class Nf3 {
@method
@async
@param {string} contractName - the name of the smart contract in question. Possible
values are 'Shield', 'State', 'Proposers', 'Challengers'.
values are 'Shield', 'State', 'Proposers', 'Challenges'.
@returns {Promise} Resolves into the Ethereum address of the contract
*/
async getContractAbi(contractName) {
Expand All @@ -428,7 +424,7 @@ class Nf3 {
@method
@async
@param {string} contractName - the name of the smart contract in question. Possible
values are 'Shield', 'State', 'Proposers', 'Challengers'.
values are 'Shield', 'State', 'Proposers', 'Challenges'.
@returns {Promise} Resolves into the Ethereum ABI of the contract
*/
async getContractAbiOptimist(contractName) {
Expand All @@ -441,7 +437,7 @@ class Nf3 {
@method
@async
@param {string} contractName - the name of the smart contract in question. Possible
values are 'Shield', 'State', 'Proposers', 'Challengers'.
values are 'Shield', 'State', 'Proposers', 'Challenges'.
@returns {Promise} Resolves into the Ethereum address of the contract
*/
async getContractAddress(contractName) {
Expand All @@ -454,7 +450,7 @@ class Nf3 {
@method
@async
@param {string} contractName - the name of the smart contract in question. Possible
values are 'Shield', 'State', 'Proposers', 'Challengers'.
values are 'Shield', 'State', 'Proposers', 'Challenges'.
@returns {Promise} Resolves into the Ethereum address of the contract
*/
async getContractAddressOptimist(contractName) {
Expand Down Expand Up @@ -1050,98 +1046,74 @@ class Nf3 {
*/
async startChallenger() {
const challengeEmitter = this.createEmitter();
const connection = new ReconnectingWebSocket(this.optimistWsUrl, [], { WebSocket });

this.websockets.push(connection); // save so we can close it properly later

/*
we can't setup up a ping until the connection is made because the ping function
only exists in the underlying 'ws' object (_ws) and that is undefined until the
websocket is opened, it seems. Hence, we put all this code inside the onopen.
*/
connection.onopen = () => {
// setup a ping every 15s
this.intervalIDs.push(
setInterval(() => {
connection._ws.ping();
}, WEBSOCKET_PING_TIME),
);
// and a listener for the pong
logger.debug('Challenge websocket connection opened');

connection.send('challenge');
};

connection.onmessage = async message => {
const msg = JSON.parse(message.data);
const { type, txDataToSign, sender } = msg;

logger.debug(`Challenger received websocket message of type ${type}`);

// if we're about to challenge, check it's actually our challenge, so as not to waste gas
if (type === 'challenge' && sender !== this.ethereumAddress) return null;
if (type === 'commit' || type === 'challenge') {
// Get the function selector from the encoded ABI, which corresponds to the first 4 bytes.
// In hex, it will correspond to the first 8 characters + 2 extra characters (0x), hence we
// do slice(0,10)
const txSelector = txDataToSign.slice(0, 10);
challengerQueue.push(async () => {
try {
const receipt = await this.submitTransaction(
txDataToSign,
this.challengesContractAddress,
0,
);
challengeEmitter.emit('receipt', receipt, type, txSelector);
} catch (err) {
logger.error({
msg: 'Error while trying to challenge a block',
type,
err,
});
challengeEmitter.emit('error', err, type, txSelector);
}
});
logger.debug(`queued ${type} ${txDataToSign}`);
}
if (type === 'rollback') {
challengeEmitter.emit('rollback', 'rollback complete');
}
return null;
};
connection.onerror = () => logger.error('websocket connection error');
connection.onclosed = () => logger.warn('websocket connection closed');
// const connection = new ReconnectingWebSocket(this.optimistWsUrl, [], { WebSocket });

// this.websockets.push(connection); // save so we can close it properly later

// /*
// we can't setup up a ping until the connection is made because the ping function
// only exists in the underlying 'ws' object (_ws) and that is undefined until the
// websocket is opened, it seems. Hence, we put all this code inside the onopen.
// */
// connection.onopen = () => {
// // setup a ping every 15s
// this.intervalIDs.push(
// setInterval(() => {
// connection._ws.ping();
// }, WEBSOCKET_PING_TIME),
// );
// // and a listener for the pong
// logger.debug('Challenge websocket connection opened');

// connection.send('challenge');
// };

// connection.onmessage = async message => {
// const msg = JSON.parse(message.data);
// const { type, txDataToSign, sender } = msg;

// logger.debug(`Challenger received websocket message of type ${type}`);

// // if we're about to challenge, check it's actually our challenge, so as not to waste gas
// if (type === 'challenge' && sender !== this.ethereumAddress) return null;
// if (type === 'commit' || type === 'challenge') {
// // Get the function selector from the encoded ABI, which corresponds to the first 4 bytes.
// // In hex, it will correspond to the first 8 characters + 2 extra characters (0x), hence we
// // do slice(0,10)
// const txSelector = txDataToSign.slice(0, 10);
// challengerQueue.push(async () => {
// try {
// const receipt = await this.submitTransaction(
// txDataToSign,
// this.challengesContractAddress,
// 0,
// );
// challengeEmitter.emit('receipt', receipt, type, txSelector);
// } catch (err) {
// logger.error({
// msg: 'Error while trying to challenge a block',
// type,
// err,
// });
// challengeEmitter.emit('error', err, type, txSelector);
// }
// });
// logger.debug(`queued ${type} ${txDataToSign}`);
// }
// if (type === 'rollback') {
// challengeEmitter.emit('rollback', 'rollback complete');
// }
// return null;
// };
// connection.onerror = () => logger.error('websocket connection error');
// connection.onclosed = () => logger.warn('websocket connection closed');
return challengeEmitter;
}

// method to turn challenges off and on. Note, this does not affect the queue
challengeEnable(enable) {
async challengeEnable(enable) {
return axios.post(`${this.optimistBaseUrl}/challenger/enable`, { enable });
}

// eslint-disable-next-line class-methods-use-this
pauseQueueChallenger() {
return new Promise(resolve => {
if (challengerQueue.autostart) {
// put an event at the head of the queue which will cleanly pause it.
challengerQueue.unshift(async () => {
challengerQueue.autostart = false;
challengerQueue.stop();
logger.info(`queue challengerQueue has been paused`);
resolve();
});
} else {
resolve();
}
});
}

// eslint-disable-next-line class-methods-use-this
unpauseQueueChallenger() {
challengerQueue.autostart = true;
challengerQueue.unshift(async () => logger.info(`queue challengerQueue has been unpaused`));
}

/**
Returns the balance of tokens held in layer 2
@method
Expand Down
13 changes: 7 additions & 6 deletions nightfall-optimist/src/event-handlers/challenge-commit.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,22 @@ async function committedToChallengeEventHandler(data) {
const { commitHash, sender } = data.returnValues;

logger.debug({
msg: 'Received commmitted to challenge event',
msg: 'Received CommittedToChallenge event',
commitHash,
sender,
});

logger.info('A challenge commitment has been mined');

const commitData = await getCommit(commitHash);
// We may not find the commitment. In this case, it's probably not ours so we take no action
// We may not find the commitment (probably not ours), so we take no action
if (commitData === null) {
logger.debug('Commit hash not found in database');
return;
}
// if retrieved is true, then we've looked up this commit before and we assume
// we have already revealed it - thus we don't reveal it again because that would
// just waste gas. This could happen if a chain reorg were to re-emit the

// If `retrieved` is true, then we've looked up this commit before and we assume
// we have already revealed it so we take no action.
// This could happen if a chain reorg were to re-emit the
// CommittedToChallenge event.
const { txDataToSign, retrieved } = commitData;
if (!retrieved) revealChallenge(txDataToSign, sender);
Expand Down
13 changes: 2 additions & 11 deletions nightfall-optimist/src/event-handlers/index.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import {
startEventQueue,
subscribeToChallengeWebSocketConnection,
subscribeToInstantWithDrawalWebSocketConnection,
} from './subscribe.mjs';
import { startEventQueue, subscribeToInstantWithDrawalWebSocketConnection } from './subscribe.mjs';
import blockProposedEventHandler from './block-proposed.mjs';
import newCurrentProposerEventHandler from './new-current-proposer.mjs';
import transactionSubmittedEventHandler from './transaction-submitted.mjs';
Expand Down Expand Up @@ -39,9 +35,4 @@ const eventHandlers = {
},
};

export {
startEventQueue,
subscribeToChallengeWebSocketConnection,
subscribeToInstantWithDrawalWebSocketConnection,
eventHandlers,
};
export { startEventQueue, subscribeToInstantWithDrawalWebSocketConnection, eventHandlers };
10 changes: 1 addition & 9 deletions nightfall-optimist/src/event-handlers/rollback.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/
import logger from '@polygon-nightfall/common-files/utils/logger.mjs';
import constants from '@polygon-nightfall/common-files/constants/index.mjs';
import { dequeueEvent, enqueueEvent } from '@polygon-nightfall/common-files/utils/event-queue.mjs';
import { dequeueEvent } from '@polygon-nightfall/common-files/utils/event-queue.mjs';
import {
addTransactionsToMemPool,
deleteBlock,
Expand All @@ -17,10 +17,6 @@ import {
} from '../services/database.mjs';
import Block from '../classes/block.mjs';
import { checkTransaction } from '../services/transaction-checker.mjs';
import {
signalRollbackCompleted as signalRollbackCompletedToChallenger,
isMakeChallengesEnable,
} from '../services/challenges.mjs';

const { ZERO } = constants;

Expand Down Expand Up @@ -132,10 +128,6 @@ async function rollbackEventHandler(data) {
await dequeueEvent(2); // Remove an event from the stopQueue.
// A Rollback triggers a NewCurrentProposer event which should trigger queue[0].end()
// But to be safe we enqueue a helper event to guarantee queue[0].end() runs.

// assumption is if optimist has makeChallenges ON there is challenger
// websocket client waiting for signal rollback
if (isMakeChallengesEnable()) await enqueueEvent(() => signalRollbackCompletedToChallenger(), 0);
}

export default rollbackEventHandler;
12 changes: 0 additions & 12 deletions nightfall-optimist/src/event-handlers/subscribe.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -76,18 +76,6 @@ export async function startEventQueue(callback, ...arg) {
return emitters;
}

export async function subscribeToChallengeWebSocketConnection(callback, ...args) {
wss.on('connection', ws => {
ws.on('message', message => {
if (message === 'challenge') {
setupWebsocketEvents(ws, 'challenge');
callback(ws, args);
}
});
});
logger.debug('Subscribed to Challenge WebSocket connection');
}

export async function subscribeToInstantWithDrawalWebSocketConnection(callback, ...args) {
wss.on('connection', ws => {
ws.on('message', message => {
Expand Down
3 changes: 0 additions & 3 deletions nightfall-optimist/src/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,11 @@ import { checkContractsABI } from '@polygon-nightfall/common-files/utils/sync-fi
import app from './app.mjs';
import {
startEventQueue,
subscribeToChallengeWebSocketConnection,
subscribeToInstantWithDrawalWebSocketConnection,
eventHandlers,
} from './event-handlers/index.mjs';
import Proposer from './classes/proposer.mjs';
import { conditionalMakeBlock } from './services/block-assembler.mjs';
import { setChallengeWebSocketConnection } from './services/challenges.mjs';
import initialBlockSync from './services/state-sync.mjs';
import { setInstantWithdrawalWebSocketConnection } from './services/instant-withdrawal.mjs';
import { setProposer } from './routes/proposer.mjs';
Expand All @@ -28,7 +26,6 @@ const main = async () => {
const proposerEthAddress = app.get('proposerEthAddress');
autoChangeCurrentProposer(proposerEthAddress); // starts the auto change current proposer service
// subscribe to WebSocket events first
await subscribeToChallengeWebSocketConnection(setChallengeWebSocketConnection);
await subscribeToInstantWithDrawalWebSocketConnection(setInstantWithdrawalWebSocketConnection);
await startEventQueue(queueManager, eventHandlers, proposer);

Expand Down
5 changes: 3 additions & 2 deletions nightfall-optimist/src/routes/challenger.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const router = express.Router();
* security:
* - ApiKeyAuth: []
* tags:
* - Challanger
* - Challenger
* summary: Enable a challenger.
* description: TBC
* parameters:
Expand All @@ -41,11 +41,12 @@ router.post('/enable', auth, async (req, res, next) => {
const { enable } = req.body;
const result = enable === true ? startMakingChallenges() : stopMakingChallenges();
res.json(result);

if (queues[2].length === 0) {
logger.info('After enabling challenges back, no challenges remain unresolved');
} else {
logger.info(
`After enabling challenges back, there were ${queues[2].length} unresolved challenges. Running them now.`,
`After enabling challenges back, there were ${queues[2].length} unresolved challenges. Running them now.`,
);

// start queue[2] and await all the unresolved challenges being run
Expand Down
Loading