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

Westlad/fix restart nightfall #564

Merged
merged 11 commits into from
Mar 21, 2022
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
30 changes: 23 additions & 7 deletions cli/lib/nf3.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -223,15 +223,12 @@ class Nf3 {
// TODO does this still work if there is a chain reorg or do we have to handle that?
return new Promise((resolve, reject) => {
logger.debug(`Confirming transaction ${signed.transactionHash}`);
this.notConfirmed++;
this.web3.eth
.sendSignedTransaction(signed.rawTransaction)
.on('confirmation', (number, receipt) => {
if (number === 12) {
this.notConfirmed--;
logger.debug(
`Transaction ${receipt.transactionHash} has been confirmed ${number} times.`,
`Number of unconfirmed transactions is ${this.notConfirmed}`,
);
resolve(receipt);
}
Expand Down Expand Up @@ -566,22 +563,41 @@ class Nf3 {
the proposer.
@method
@async
@param {string} Proposer REST API URL with format https://xxxx.xxx.xx
@param {string} Proposer REST API URL with format https://xxxx.xxx.xx
@returns {Promise} A promise that resolves to the Ethereum transaction receipt.
*/
async registerProposer(url) {
const res = await axios.post(`${this.optimistBaseUrl}/proposer/register`, {
address: this.ethereumAddress,
url,
});
logger.debug(`Proposer Registered with address ${this.ethereumAddress} and URL ${url}`);
if (res.data.txDataToSign === '') return false; // already registered
return this.submitTransaction(
res.data.txDataToSign,
this.proposersContractAddress,
this.PROPOSER_BOND,
);
}

/**
Registers a proposer locally with the Optimist instance only. This will cause
Optimist to make blocks when this proposer is current but these will revert if
the proposer isn't registered on the blockchain too. This method is useful only
if the proposer is already registered on the blockchain (has paid their bond) and
for some reason the Optimist instance does not know about them, e.g. a new instance
has been created. The method 'registerProposer' will both register the proposer
with the blockchain and register locally with the optimist instance. So, if
that method has been used successfully, there is no need to also call this method
@method
@async
@returns {Promise} A promise that resolves to the Ethereum transaction receipt.
*/
async registerProposerLocally() {
Westlad marked this conversation as resolved.
Show resolved Hide resolved
return axios.post(`${this.optimistBaseUrl}/proposer/registerlocally`, {
Westlad marked this conversation as resolved.
Show resolved Hide resolved
address: this.ethereumAddress,
});
}

/**
De-registers an existing proposer.
It will use the address of the Ethereum Signing key that is holds to de-register
Expand Down Expand Up @@ -652,7 +668,7 @@ class Nf3 {
Update Proposers URL
@method
@async
@param {string} Proposer REST API URL with format https://xxxx.xxx.xx
@param {string} Proposer REST API URL with format https://xxxx.xxx.xx
@returns {array} A promise that resolves to the Ethereum transaction receipt.
*/
async updateProposer(url) {
Expand Down Expand Up @@ -714,7 +730,7 @@ class Nf3 {
Send offchain transaction to Optimist
@method
@async
@param {string} transaction
@param {string} transaction
@returns {array} A promise that resolves to the API call status
*/
async sendOffchainTransaction(transaction) {
Expand Down
1 change: 0 additions & 1 deletion common-files/utils/contract.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,6 @@ export async function waitForContract(contractName) {
try {
error = undefined;
const address = await getContractAddress(contractName); // eslint-disable-line no-await-in-loop
logger.debug(`${contractName} contract address is ${address}`);
if (address === undefined) throw new Error(`${contractName} contract address was undefined`);
instance = getContractInstance(contractName, address);
return instance;
Expand Down
3 changes: 2 additions & 1 deletion config/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ const { DOMAIN_NAME = '' } = process.env;
module.exports = {
COMMITMENTS_DB: 'nightfall_commitments',
OPTIMIST_DB: 'optimist_data',
METADATA_COLLECTION: 'metadata',
PROPOSER_COLLECTION: 'proposers',
CHALLENGER_COLLECTION: 'challengers',
TRANSACTIONS_COLLECTION: 'transactions',
SUBMITTED_BLOCKS_COLLECTION: 'blocks',
NULLIFIER_COLLECTION: 'nullifiers',
Expand Down
68 changes: 0 additions & 68 deletions doc/contract-update.md

This file was deleted.

Binary file removed doc/contract-upgrade.png
Binary file not shown.
3 changes: 1 addition & 2 deletions nightfall-client/src/event-handlers/subscribe.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,14 @@ const { STATE_CONTRACT_NAME, RETRIES } = config;
* This is useful in case nightfall-client comes up before the contract
* is fully deployed.
*/
async function waitForContract(contractName) {
export async function waitForContract(contractName) {
let errorCount = 0;
let error;
let instance;
while (errorCount < RETRIES) {
try {
error = undefined;
const address = await getContractAddress(contractName);
logger.debug(`${contractName} contract address is ${address}`);
if (address === undefined) throw new Error(`${contractName} contract address was undefined`);
instance = getContractInstance(contractName, address);
return instance;
Expand Down
3 changes: 2 additions & 1 deletion nightfall-client/src/services/database.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ export async function saveBlock(_block) {
if (!existing || !existing.blockNumber) {
return db.collection(SUBMITTED_BLOCKS_COLLECTION).updateOne(query, update, { upsert: true });
}
throw new Error('Attempted to replay existing layer 2 block');
logger.warn('Attempted to replay existing layer 2 block. This is expected if we are syncing');
Westlad marked this conversation as resolved.
Show resolved Hide resolved
return true;
}

/**
Expand Down
7 changes: 2 additions & 5 deletions nightfall-client/src/services/state-sync.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,20 @@ their local commitments databsae.

import config from 'config';
import logger from 'common-files/utils/logger.mjs';
import { getContractInstance } from 'common-files/utils/contract.mjs';
import mongo from 'common-files/utils/mongo.mjs';
import { waitForContract } from '../event-handlers/subscribe.mjs';
import blockProposedEventHandler from '../event-handlers/block-proposed.mjs';
import rollbackEventHandler from '../event-handlers/rollback.mjs';

const { MONGO_URL, COMMITMENTS_DB, COMMITMENTS_COLLECTION, STATE_CONTRACT_NAME } = config;

const syncState = async (fromBlock = 'earliest', toBlock = 'latest', eventFilter = 'allEvents') => {
const stateContractInstance = await getContractInstance(STATE_CONTRACT_NAME); // Rollback, BlockProposed
const stateContractInstance = await waitForContract(STATE_CONTRACT_NAME); // Rollback, BlockProposed

const pastStateEvents = await stateContractInstance.getPastEvents(eventFilter, {
fromBlock,
toBlock,
});
logger.info(`pastStateEvents: ${JSON.stringify(pastStateEvents)}`);

for (let i = 0; i < pastStateEvents.length; i++) {
switch (pastStateEvents[i].event) {
Expand All @@ -46,8 +45,6 @@ const genGetCommitments = async (query = {}, proj = {}) => {
// eslint-disable-next-line import/prefer-default-export
export const initialClientSync = async () => {
const allCommitments = await genGetCommitments();
if (allCommitments.length === 0) return {};

const commitmentBlockNumbers = allCommitments.map(a => a.blockNumber).filter(n => n >= 0);
logger.info(`commitmentBlockNumbers: ${commitmentBlockNumbers}`);
const firstSeenBlockNumber = Math.min(...commitmentBlockNumbers);
Expand Down
11 changes: 10 additions & 1 deletion nightfall-deployer/src/index.mjs
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import Web3 from 'common-files/utils/web3.mjs';
import logger from 'common-files/utils/logger.mjs';
import circuits from './circuit-setup.mjs';
import setupContracts from './contract-setup.mjs';

// TODO these can be paralleled
async function main() {
await circuits.waitForZokrates();
await circuits.setupCircuits();
await setupContracts();
try {
await setupContracts();
} catch (err) {
if (err.message.includes('Transaction has been reverted by the EVM'))
logger.warn(
'Writing contract addresses to the State contract failed. This is probably because they are aready set. Did you already run deployer?',
);
else throw new Error(err);
}
Web3.disconnect();
}

Expand Down
1 change: 0 additions & 1 deletion nightfall-optimist/src/event-handlers/subscribe.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ export async function waitForContract(contractName) {
try {
error = undefined;
const address = await getContractAddress(contractName);
logger.debug(`${contractName} contract address is ${address}`);
if (address === undefined) throw new Error(`${contractName} contract address was undefined`);
instance = getContractInstance(contractName, address);
return instance;
Expand Down
2 changes: 2 additions & 0 deletions nightfall-optimist/src/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ import {
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';
import { setBlockProposedWebSocketConnection } from './event-handlers/block-proposed.mjs';

const main = async () => {
try {
const proposer = new Proposer();
setProposer(proposer); // passes the proposer instance int the proposer routes
// subscribe to WebSocket events first
await subscribeToBlockAssembledWebSocketConnection(setBlockAssembledWebSocketConnection);
await subscribeToChallengeWebSocketConnection(setChallengeWebSocketConnection);
Expand Down
67 changes: 44 additions & 23 deletions nightfall-optimist/src/routes/proposer.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,29 @@ import config from 'config';
import Timber from 'common-files/classes/timber.mjs';
import logger from 'common-files/utils/logger.mjs';
import { getContractInstance } from 'common-files/utils/contract.mjs';
import { enqueueEvent } from 'common-files/utils/event-queue.mjs';
import Block from '../classes/block.mjs';
import { Transaction, TransactionError } from '../classes/index.mjs';
import {
setRegisteredProposerAddress,
isRegisteredProposerAddressMine,
deleteRegisteredProposerAddress,
getMempoolTransactions,
getLatestTree,
getLatestBlockInfo,
} from '../services/database.mjs';
import { waitForContract } from '../event-handlers/subscribe.mjs';
import transactionSubmittedEventHandler from '../event-handlers/transaction-submitted.mjs';
import getProposers from '../services/proposer.mjs';

const router = express.Router();
const { STATE_CONTRACT_NAME, PROPOSERS_CONTRACT_NAME, SHIELD_CONTRACT_NAME, ZERO } = config;

let proposer;
export function setProposer(p) {
proposer = p;
}

/**
* Function to return a raw transaction that registers a proposer. This just
* provides the tx data, the user will need to append the registration bond
Expand All @@ -33,12 +41,39 @@ router.post('/register', async (req, res, next) => {
logger.debug(`register proposer endpoint received POST ${JSON.stringify(req.body, null, 2)}`);
try {
const { address, url = '' } = req.body;
const proposersContractInstance = await getContractInstance(PROPOSERS_CONTRACT_NAME);
const txDataToSign = await proposersContractInstance.methods.registerProposer(url).encodeABI();
logger.debug('returning raw transaction data');
logger.silly(`raw transaction is ${JSON.stringify(txDataToSign, null, 2)}`);
const proposersContractInstance = await waitForContract(PROPOSERS_CONTRACT_NAME);
// the first thing to do is to check if the proposer is already registered on the blockchain
const proposers = (await getProposers()).map(p => p.thisAddress);
// if not, let's register it
let txDataToSign = '';
if (!proposers.includes(address)) {
txDataToSign = await proposersContractInstance.methods.registerProposer(url).encodeABI();
} else
logger.warn(
'Proposer was already registered on the blockchain - registration attempt ignored',
);
// when we get to here, either the proposer was already registered (txDataToSign === '')
// or we're just about to register them. We may or may not be registed locally
// with optimist though. Let's check and fix that if needed.
if (!(await isRegisteredProposerAddressMine(address))) {
logger.debug('Registering proposer locally');
await setRegisteredProposerAddress(address, url); // save the registration address
// We've just registered with optimist but if we were already registered on the blockchain,
// we should check if we're the current proposer and, if so, set things up so we start
// making blocks immediately
if (txDataToSign === '') {
logger.warn(
'Proposer was already registered on the blockchain but not with this Optimist instance - registering locally',
);
const stateContractInstance = await waitForContract(STATE_CONTRACT_NAME);
const currentProposer = await stateContractInstance.methods.getCurrentProposer().call();
if (address === currentProposer.thisAddress) {
proposer.isMe = true;
await enqueueEvent(() => logger.info('Start Queue'), 0); // kickstart the queue
}
}
}
res.json({ txDataToSign });
setRegisteredProposerAddress(address, url); // save the registration address and URL
} catch (err) {
logger.error(err);
next(err);
Expand Down Expand Up @@ -93,22 +128,8 @@ router.get('/current-proposer', async (req, res, next) => {
router.get('/proposers', async (req, res, next) => {
logger.debug(`list proposals endpoint received GET`);
try {
const stateContractInstance = await getContractInstance(STATE_CONTRACT_NAME);
// proposers is an on-chain mapping so to get proposers we need to key to start iterating
// the safest to start with is the currentProposer
const currentProposer = await stateContractInstance.methods.currentProposer().call();
const proposers = [];
let thisPtr = currentProposer.thisAddress;
// Loop through the circular list until we run back into the currentProposer.
do {
// eslint-disable-next-line no-await-in-loop
const proposer = await stateContractInstance.methods.proposers(thisPtr).call();
proposers.push(proposer);
thisPtr = proposer.nextAddress;
} while (thisPtr !== currentProposer.thisAddress);

logger.debug('returning raw transaction data');
logger.silly(`raw transaction is ${JSON.stringify(proposers, null, 2)}`);
const proposers = await getProposers();
logger.debug(`Returning proposer list of length ${proposers.length}`);
res.json({ proposers });
} catch (err) {
logger.error(err);
Expand Down Expand Up @@ -203,14 +224,14 @@ router.post('/propose', async (req, res, next) => {
logger.debug(`propose endpoint received POST`);
logger.silly(`With content ${JSON.stringify(req.body, null, 2)}`);
try {
const { transactions, proposer, currentLeafCount } = req.body;
const { transactions, proposer: prop, currentLeafCount } = req.body;
const latestBlockInfo = await getLatestBlockInfo();
const latestTree = await getLatestTree();
// use the information we've been POSTED to assemble a block
// we use a Builder pattern because an async constructor is bad form
const { block } = await Block.build({
transactions,
proposer,
proposer: prop,
currentLeafCount,
latestBlockInfo: {
blockNumberL2: latestBlockInfo.blockNumberL2,
Expand Down
Loading