From 48a71a4df740ce6a4de0d334196b76f1189723e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9mien=20Kocher?= <nkcr.je@gmail.com> Date: Tue, 13 Sep 2022 11:23:36 +0200 Subject: [PATCH 01/10] Fixes initialisation process on the web frontend Fix #165 --- .../src/pages/election/components/utils/useChangeAction.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/frontend/src/pages/election/components/utils/useChangeAction.tsx b/web/frontend/src/pages/election/components/utils/useChangeAction.tsx index 7ccb4b669..5ed008ae3 100644 --- a/web/frontend/src/pages/election/components/utils/useChangeAction.tsx +++ b/web/frontend/src/pages/election/components/utils/useChangeAction.tsx @@ -389,8 +389,8 @@ const useChangeAction = ( // TODO: can be modified such that if the majority of the node are // initialized than the election status can still be set to initialized - const promises = Array.from(nodeProxyAddresses.values()).map((proxy) => { - if (proxy !== '') { + const promises = Array.from(nodeProxyAddresses).map(([node, proxy]) => { + if (proxy !== '' && DKGStatuses.get(node) === NodeStatus.NotInitialized) { return initialize(proxy); } return undefined; From cda589a16529c9189760b5c7ef5964ce428d32b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9mien=20Kocher?= <nkcr.je@gmail.com> Date: Wed, 14 Sep 2022 14:14:57 +0200 Subject: [PATCH 02/10] Moves the DKG logic to DKGStatusRow --- web/frontend/src/mocks/handlers.ts | 2 +- web/frontend/src/pages/election/Show.tsx | 37 ++++- .../src/pages/election/components/Action.tsx | 9 +- .../election/components/ChooseProxyModal.tsx | 6 +- .../election/components/DKGStatusRow.tsx | 153 +++++++++++++++--- .../election/components/DKGStatusTable.tsx | 14 +- .../components/utils/useChangeAction.tsx | 134 +-------------- web/frontend/src/types/node.ts | 26 +++ 8 files changed, 211 insertions(+), 170 deletions(-) diff --git a/web/frontend/src/mocks/handlers.ts b/web/frontend/src/mocks/handlers.ts index ca53c8b75..5a06a4a67 100644 --- a/web/frontend/src/mocks/handlers.ts +++ b/web/frontend/src/mocks/handlers.ts @@ -63,7 +63,7 @@ export const handlers = [ ? { lastname: 'Bobster', firstname: 'Alice', - role: UserRole.Voter, + role: UserRole.Admin, sciper: userId, } : {}; diff --git a/web/frontend/src/pages/election/Show.tsx b/web/frontend/src/pages/election/Show.tsx index e88241497..5d7d671c0 100644 --- a/web/frontend/src/pages/election/Show.tsx +++ b/web/frontend/src/pages/election/Show.tsx @@ -9,7 +9,7 @@ import Modal from 'components/modal/Modal'; import StatusTimeline from './components/StatusTimeline'; import Loading from 'pages/Loading'; import Action from './components/Action'; -import { NodeStatus } from 'types/node'; +import { InternalDKGInfo, NodeStatus } from 'types/node'; import useGetResults from './components/utils/useGetResults'; import UserIDTable from './components/UserIDTable'; import DKGStatusTable from './components/DKGStatusTable'; @@ -42,7 +42,7 @@ const ElectionShow: FC = () => { const [nodeProxyAddresses, setNodeProxyAddresses] = useState<Map<string, string>>(new Map()); const [nodeToSetup, setNodeToSetup] = useState<[string, string]>(null); - // The status of each node + // The status of each node. Key is the node's address. const [DKGStatuses, setDKGStatuses] = useState<Map<string, NodeStatus>>(new Map()); const [nodeLoading, setNodeLoading] = useState<Map<string, boolean>>(null); @@ -51,6 +51,25 @@ const ElectionShow: FC = () => { const ongoingItem = 'ongoingAction' + electionID; const nodeToSetupItem = 'nodeToSetup' + electionID; + const notifyDKGState = (node: string, info: InternalDKGInfo) => { + console.log('DKG node updated:', info); + switch (info.getStatus()) { + case NodeStatus.Failed: + console.log('DKG node failed'); + setOngoingAction(OngoingAction.None); + break; + case NodeStatus.Setup: + setOngoingAction(OngoingAction.None); + setStatus(Status.Setup); + break; + } + + const newDKGStatuses = new Map(DKGStatuses); + newDKGStatuses.set(node, info.getStatus()); + setDKGStatuses(newDKGStatuses); + console.log('dkg statuses:', DKGStatuses); + }; + // Fetch result when available after a status change useEffect(() => { if (status === Status.ResultAvailable && isResultAvailable) { @@ -76,6 +95,7 @@ const ElectionShow: FC = () => { const storedOngoingAction = JSON.parse(window.localStorage.getItem(ongoingItem)); if (storedOngoingAction !== null) { + console.log('stored ongoing action:', storedOngoingAction); setOngoingAction(storedOngoingAction); } @@ -141,14 +161,20 @@ const ElectionShow: FC = () => { // TODO: can be modified such that if the majority of the node are // initialized than the election status can still be set to initialized - if (statuses.includes(NodeStatus.NotInitialized)) return; + if (statuses.includes(NodeStatus.NotInitialized)) { + setOngoingAction(OngoingAction.None); + setStatus(Status.Initial); + return; + } if (statuses.includes(NodeStatus.Setup)) { + setOngoingAction(OngoingAction.None); setStatus(Status.Setup); return; } if (statuses.includes(NodeStatus.Unreachable)) return; + if (statuses.includes(NodeStatus.Failed)) return; setStatus(Status.Initialized); @@ -220,8 +246,6 @@ const ElectionShow: FC = () => { setOngoingAction={setOngoingAction} nodeToSetup={nodeToSetup} setNodeToSetup={setNodeToSetup} - DKGStatuses={DKGStatuses} - setDKGStatuses={setDKGStatuses} /> )} </div> @@ -248,6 +272,9 @@ const ElectionShow: FC = () => { setDKGStatuses={setDKGStatuses} setTextModalError={setTextModalError} setShowModalError={setShowModalError} + ongoingAction={ongoingAction} + notifyDKGState={notifyDKGState} + nodeToSetup={nodeToSetup} /> </div> </div> diff --git a/web/frontend/src/pages/election/components/Action.tsx b/web/frontend/src/pages/election/components/Action.tsx index b08f0e035..ed5319dbb 100644 --- a/web/frontend/src/pages/election/components/Action.tsx +++ b/web/frontend/src/pages/election/components/Action.tsx @@ -3,7 +3,6 @@ import PropTypes from 'prop-types'; import { ID } from 'types/configuration'; import { OngoingAction, Status } from 'types/election'; -import { NodeStatus } from 'types/node'; import useChangeAction from './utils/useChangeAction'; type ActionProps = { @@ -19,8 +18,6 @@ type ActionProps = { setOngoingAction: (action: OngoingAction) => void; nodeToSetup: [string, string]; setNodeToSetup: ([node, proxy]: [string, string]) => void; - DKGStatuses: Map<string, NodeStatus>; - setDKGStatuses: (dkgStatuses: Map<string, NodeStatus>) => void; }; const Action: FC<ActionProps> = ({ @@ -36,8 +33,6 @@ const Action: FC<ActionProps> = ({ setOngoingAction, nodeToSetup, setNodeToSetup, - DKGStatuses, - setDKGStatuses, }) => { const { getAction, modalClose, modalCancel, modalDelete, modalSetup } = useChangeAction( status, @@ -51,9 +46,7 @@ const Action: FC<ActionProps> = ({ ongoingAction, setOngoingAction, nodeToSetup, - setNodeToSetup, - DKGStatuses, - setDKGStatuses + setNodeToSetup ); return ( diff --git a/web/frontend/src/pages/election/components/ChooseProxyModal.tsx b/web/frontend/src/pages/election/components/ChooseProxyModal.tsx index 060420101..fe3bcc6c5 100644 --- a/web/frontend/src/pages/election/components/ChooseProxyModal.tsx +++ b/web/frontend/src/pages/election/components/ChooseProxyModal.tsx @@ -2,12 +2,10 @@ import { Dialog, Transition } from '@headlessui/react'; import { CogIcon } from '@heroicons/react/outline'; import { FC, Fragment, useRef } from 'react'; import { useTranslation } from 'react-i18next'; -import { NodeStatus } from 'types/node'; type ChooseProxyModalProps = { roster: string[]; showModal: boolean; - DKGStatuses: Map<string, NodeStatus>; nodeProxyAddresses: Map<string, string>; nodeToSetup: [string, string]; setNodeToSetup: (node: [string, string]) => void; @@ -18,7 +16,6 @@ type ChooseProxyModalProps = { const ChooseProxyModal: FC<ChooseProxyModalProps> = ({ roster, showModal, - DKGStatuses, nodeProxyAddresses, nodeToSetup, setNodeToSetup, @@ -48,8 +45,7 @@ const ChooseProxyModal: FC<ChooseProxyModalProps> = ({ nodeToSetup !== null && roster.map((node, index) => { const proxy = nodeProxyAddresses.get(node); - const status = DKGStatuses.get(node); - const checkable = proxy !== '' && status === NodeStatus.Initialized; + const checkable = proxy !== ''; return ( <div className="flex items-center my-4 ml-4" key={node}> diff --git a/web/frontend/src/pages/election/components/DKGStatusRow.tsx b/web/frontend/src/pages/election/components/DKGStatusRow.tsx index 1f81eb185..0819dbd9f 100644 --- a/web/frontend/src/pages/election/components/DKGStatusRow.tsx +++ b/web/frontend/src/pages/election/components/DKGStatusRow.tsx @@ -2,10 +2,17 @@ import React, { FC, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import * as endpoints from 'components/utils/Endpoints'; -import { NodeProxyAddress, NodeStatus } from 'types/node'; +import { DKGInfo, InternalDKGInfo, NodeProxyAddress, NodeStatus } from 'types/node'; import { ID } from 'types/configuration'; import DKGStatus from 'components/utils/DKGStatus'; import IndigoSpinnerIcon from './IndigoSpinnerIcon'; +import { OngoingAction } from 'types/election'; +import Modal from 'components/modal/Modal'; +import { ExclamationCircleIcon } from '@heroicons/react/outline'; +import { pollDKG } from './utils/PollStatus'; + +const POLLING_INTERVAL = 1000; +const MAX_ATTEMPTS = 10; type DKGStatusRowProps = { electionId: ID; @@ -19,26 +26,34 @@ type DKGStatusRowProps = { setDKGStatuses: (DKFStatuses: Map<string, NodeStatus>) => void; setTextModalError: (error: string) => void; setShowModalError: (show: boolean) => void; + // notify to start initialization + ongoingAction: OngoingAction; + // notify the parent of the new state + notifyDKGState: (node: string, info: InternalDKGInfo) => void; + nodeToSetup: [string, string]; }; const DKGStatusRow: FC<DKGStatusRowProps> = ({ electionId, - node, + node, // node is the node address, not the proxy index, loading, setLoading, nodeProxyAddresses, setNodeProxyAddresses, - DKGStatuses, - setDKGStatuses, setTextModalError, setShowModalError, + ongoingAction, + notifyDKGState, + nodeToSetup, }) => { const { t } = useTranslation(); const [proxy, setProxy] = useState(null); const [DKGLoading, setDKGLoading] = useState(true); const [status, setStatus] = useState<NodeStatus>(null); + const [info, setInfo] = useState(''); + const abortController = new AbortController(); const signal = abortController.signal; const TIMEOUT = 10000; @@ -50,13 +65,96 @@ const DKGStatusRow: FC<DKGStatusRowProps> = ({ signal: signal, }; + const [showModal, setShowModal] = useState(false); + + // Notify the parent each time our status changes useEffect(() => { - // update status on useChangeAction (i.e. initializing and setting up) - if (DKGStatuses.has(node)) { - setStatus(DKGStatuses.get(node)); + notifyDKGState(node, InternalDKGInfo.fromStatus(status)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [status]); + + const initializeNode = async () => { + const req = { + method: 'POST', + body: JSON.stringify({ + ElectionID: electionId, + Proxy: proxy, + }), + headers: { + 'Content-Type': 'application/json', + }, + }; + + const response = await fetch(endpoints.dkgActors, req); + if (!response.ok) { + const txt = await response.text(); + throw new Error(txt); + } + }; + + // Signal the start of DKG initialization + useEffect(() => { + if ( + ongoingAction === OngoingAction.Initializing && + (status === NodeStatus.NotInitialized || status === NodeStatus.Failed) + ) { + // TODO: can be modified such that if the majority of the node are + // initialized than the election status can still be set to initialized + + setDKGLoading(true); + + initializeNode() + .then(() => { + setStatus(NodeStatus.Initialized); + }) + .catch((e) => { + setInfo(e.toString()); + setStatus(NodeStatus.Failed); + }); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [DKGStatuses]); + }, [ongoingAction]); + + const pollDKGStatus = (statusToMatch: NodeStatus) => { + const req = { + method: 'GET', + signal: signal, + }; + + const match = (s: NodeStatus) => s === statusToMatch; + + return pollDKG( + endpoints.getDKGActors(proxy, electionId), + req, + match, + POLLING_INTERVAL, + MAX_ATTEMPTS + ); + }; + + useEffect(() => { + if ( + ongoingAction === OngoingAction.SettingUp && + nodeToSetup !== null && + nodeToSetup[0] === node + ) { + setDKGLoading(true); + pollDKGStatus(NodeStatus.Setup) + .then( + () => { + setStatus(NodeStatus.Setup); + }, + (reason: any) => { + setStatus(NodeStatus.Failed); + setInfo(reason.toString()); + } + ) + .catch((e) => { + console.log('error:', e); + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ongoingAction]); // Set the mapping of the node and proxy address (only if the address was not // already fetched) @@ -96,6 +194,8 @@ const DKGStatusRow: FC<DKGStatusRowProps> = ({ } }; + setDKGLoading(true); + fetchNodeProxy().then((nodeProxyAddress) => { setProxy(nodeProxyAddress.Proxy); }); @@ -116,12 +216,12 @@ const DKGStatusRow: FC<DKGStatusRowProps> = ({ // Fetch the status of the nodes useEffect(() => { if (proxy !== null && status === null) { - const fetchDKGStatus = async () => { + const fetchDKGStatus = async (): Promise<InternalDKGInfo> => { // If we were not able to retrieve the proxy address of the node, // still return a resolved promise so that promise.then() goes to onSuccess(). // Error was already displayed, no need to throw another one. if (proxy === '') { - return NodeStatus.Unreachable; + return InternalDKGInfo.fromStatus(NodeStatus.Unreachable); } try { @@ -132,7 +232,7 @@ const DKGStatusRow: FC<DKGStatusRowProps> = ({ const response = await fetch(endpoints.getDKGActors(proxy, electionId), request); if (response.status === 404) { - return NodeStatus.NotInitialized; + return InternalDKGInfo.fromStatus(NodeStatus.NotInitialized); } if (!response.ok) { @@ -141,7 +241,8 @@ const DKGStatusRow: FC<DKGStatusRowProps> = ({ } let dataReceived = await response.json(); - return dataReceived.Status as NodeStatus; + console.log('data received:', dataReceived); + return InternalDKGInfo.fromInfo(dataReceived as DKGInfo); } catch (e) { let errorMessage = t('errorRetrievingNodes'); @@ -157,12 +258,13 @@ const DKGStatusRow: FC<DKGStatusRowProps> = ({ // if we could not retrieve the proxy still resolve the promise // so that promise.then() goes to onSuccess() but display the error - return NodeStatus.Unreachable; + return InternalDKGInfo.fromStatus(NodeStatus.Unreachable); } }; - fetchDKGStatus().then((nodeStatus) => { - setStatus(nodeStatus); + fetchDKGStatus().then((internalStatus) => { + setStatus(internalStatus.getStatus()); + setInfo(internalStatus.getError()); }); } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -173,11 +275,6 @@ const DKGStatusRow: FC<DKGStatusRowProps> = ({ if (status !== null) { setDKGLoading(false); - // notify parent - const newDKGStatuses = new Map(DKGStatuses); - newDKGStatuses.set(node, status); - setDKGStatuses(newDKGStatuses); - const newLoading = new Map(loading); newLoading.set(node, false); setLoading(newLoading); @@ -190,8 +287,22 @@ const DKGStatusRow: FC<DKGStatusRowProps> = ({ <td className="px-1.5 sm:px-6 py-4 font-medium text-gray-900 whitespace-nowrap truncate"> {t('node')} {index} ({node}) </td> - <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> + <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 flex flex-row"> {!DKGLoading ? <DKGStatus status={status} /> : <IndigoSpinnerIcon />} + <Modal + textModal={info} + buttonRightText="close" + setShowModal={setShowModal} + showModal={showModal} + /> + {info !== '' && ( + <button + onClick={() => { + setShowModal(true); + }}> + <ExclamationCircleIcon className="ml-3 mr-2 h-5 w-5 stroke-orange-800" /> + </button> + )} </td> </tr> ); diff --git a/web/frontend/src/pages/election/components/DKGStatusTable.tsx b/web/frontend/src/pages/election/components/DKGStatusTable.tsx index 4214f264a..53d451480 100644 --- a/web/frontend/src/pages/election/components/DKGStatusTable.tsx +++ b/web/frontend/src/pages/election/components/DKGStatusTable.tsx @@ -1,7 +1,8 @@ import { FC } from 'react'; import { useTranslation } from 'react-i18next'; import { ID } from 'types/configuration'; -import { NodeStatus } from 'types/node'; +import { OngoingAction } from 'types/election'; +import { InternalDKGInfo, NodeStatus } from 'types/node'; import DKGStatusRow from './DKGStatusRow'; type DKGStatusTableProps = { @@ -15,6 +16,11 @@ type DKGStatusTableProps = { setDKGStatuses: (DKFStatuses: Map<string, NodeStatus>) => void; setTextModalError: (error: string) => void; setShowModalError: (show: boolean) => void; + // notify to start initialization + ongoingAction: OngoingAction; + // notify the parent of the new state + notifyDKGState: (node: string, info: InternalDKGInfo) => void; + nodeToSetup: [string, string]; }; const DKGStatusTable: FC<DKGStatusTableProps> = ({ @@ -28,6 +34,9 @@ const DKGStatusTable: FC<DKGStatusTableProps> = ({ setDKGStatuses, setTextModalError, setShowModalError, + ongoingAction, + notifyDKGState, + nodeToSetup, }) => { const { t } = useTranslation(); @@ -61,6 +70,9 @@ const DKGStatusTable: FC<DKGStatusTableProps> = ({ setDKGStatuses={setDKGStatuses} setTextModalError={setTextModalError} setShowModalError={setShowModalError} + ongoingAction={ongoingAction} + notifyDKGState={notifyDKGState} + nodeToSetup={nodeToSetup} /> ))} </tbody> diff --git a/web/frontend/src/pages/election/components/utils/useChangeAction.tsx b/web/frontend/src/pages/election/components/utils/useChangeAction.tsx index 5ed008ae3..a6aa2e6a5 100644 --- a/web/frontend/src/pages/election/components/utils/useChangeAction.tsx +++ b/web/frontend/src/pages/election/components/utils/useChangeAction.tsx @@ -4,8 +4,7 @@ import { useTranslation } from 'react-i18next'; import * as endpoints from 'components/utils/Endpoints'; import { ID } from 'types/configuration'; import { Action, OngoingAction, Status } from 'types/election'; -import { pollDKG, pollElection } from './PollStatus'; -import { NodeStatus } from 'types/node'; +import { pollElection } from './PollStatus'; import { AuthContext, FlashContext, FlashLevel, ProxyContext } from 'index'; import { useNavigate } from 'react-router'; import { ROUTE_ELECTION_INDEX } from 'Routes'; @@ -24,7 +23,6 @@ import OpenButton from '../ActionButtons/OpenButton'; import ResultButton from '../ActionButtons/ResultButton'; import ShuffleButton from '../ActionButtons/ShuffleButton'; import VoteButton from '../ActionButtons/VoteButton'; -import NoActionAvailable from '../ActionButtons/NoActionAvailable'; import handleLogin from 'pages/session/HandleLogin'; import { UserRole } from 'types/userRole'; @@ -40,12 +38,9 @@ const useChangeAction = ( ongoingAction: OngoingAction, setOngoingAction: (action: OngoingAction) => void, nodeToSetup: [string, string], - setNodeToSetup: ([node, proxy]: [string, string]) => void, - DKGStatuses: Map<string, NodeStatus>, - setDKGStatuses: (dkgStatuses: Map<string, NodeStatus>) => void + setNodeToSetup: ([node, proxy]: [string, string]) => void ) => { const { t } = useTranslation(); - const [isInitializing, setIsInitializing] = useState(false); const [, setIsPosting] = useState(false); const [showModalProxySetup, setShowModalProxySetup] = useState(false); @@ -101,7 +96,6 @@ const useChangeAction = ( <ChooseProxyModal roster={roster} showModal={showModalProxySetup} - DKGStatuses={DKGStatuses} nodeProxyAddresses={nodeProxyAddresses} nodeToSetup={nodeToSetup} setNodeToSetup={setNodeToSetup} @@ -123,20 +117,6 @@ const useChangeAction = ( return sendFetchRequest(endpoint, req, setIsPosting); }; - const initializeNode = async (proxy: string) => { - const request = { - method: 'POST', - body: JSON.stringify({ - ElectionID: electionID, - Proxy: proxy, - }), - headers: { - 'Content-Type': 'application/json', - }, - }; - return sendFetchRequest(endpoints.dkgActors, request, setIsPosting); - }; - const onFullFilled = (nextStatus: Status) => { if (setGetError !== null && setGetError !== undefined) { setGetError(null); @@ -185,80 +165,16 @@ const useChangeAction = ( }); }; - const pollDKGStatus = (proxy: string, statusToMatch: NodeStatus) => { - const request = { - method: 'GET', - signal: signal, - }; - - const match = (s: NodeStatus) => s === statusToMatch; - - return pollDKG( - endpoints.getDKGActors(proxy, electionID), - request, - match, - POLLING_INTERVAL, - MAX_ATTEMPTS - ); - }; - // Start to poll when there is an ongoingAction useEffect(() => { // use an abortController to stop polling when the component is unmounted switch (ongoingAction) { case OngoingAction.Initializing: - if (nodeProxyAddresses !== null) { - // TODO: can be modified such that if the majority of the node are - // initialized than the election status can still be set to initialized - const promises = Array.from(nodeProxyAddresses.values()).map((proxy) => { - if (proxy !== '') { - return pollDKGStatus(proxy, NodeStatus.Initialized); - } - return undefined; - }); - - Promise.all(promises).then( - () => { - onFullFilled(Status.Initialized); - const newDKGStatuses = new Map(DKGStatuses); - nodeProxyAddresses.forEach((proxy, node) => { - if (proxy !== '') { - newDKGStatuses.set(node, NodeStatus.Initialized); - } - }); - setDKGStatuses(newDKGStatuses); - }, - (reason: any) => onRejected(reason, Status.Initial) - ); - } + // Initializing is handled by each row of the DKG table break; case OngoingAction.SettingUp: - if (nodeToSetup !== null) { - pollDKGStatus(nodeToSetup[1], NodeStatus.Setup) - .then( - () => { - onFullFilled(Status.Setup); - const newDKGStatuses = new Map(DKGStatuses); - newDKGStatuses.set(nodeToSetup[0], NodeStatus.Setup); - setDKGStatuses(newDKGStatuses); - }, - (reason: any) => { - onRejected(reason, Status.Initialized); - - if (!(reason instanceof DOMException)) { - const newDKGStatuses = new Map(DKGStatuses); - newDKGStatuses.set(nodeToSetup[0], NodeStatus.Failed); - setDKGStatuses(newDKGStatuses); - } - } - ) - .catch((e) => { - setStatus(Status.Initialized); - setGetError(e.message); - setShowModalError(true); - }); - } + // Initializing is handled by each row of the DKG table break; case OngoingAction.Opening: pollElectionStatus(Status.Setup, Status.Open); @@ -375,34 +291,6 @@ const useChangeAction = ( // eslint-disable-next-line react-hooks/exhaustive-deps }, [userConfirmedDeleting]); - useEffect(() => { - if (isInitializing) { - const initialize = async (proxy: string) => { - setOngoingAction(OngoingAction.Initializing); - const initSuccess = await initializeNode(proxy); - - if (!initSuccess) { - setStatus(Status.Initial); - setOngoingAction(OngoingAction.None); - } - }; - - // TODO: can be modified such that if the majority of the node are - // initialized than the election status can still be set to initialized - const promises = Array.from(nodeProxyAddresses).map(([node, proxy]) => { - if (proxy !== '' && DKGStatuses.get(node) === NodeStatus.NotInitialized) { - return initialize(proxy); - } - return undefined; - }); - - Promise.all(promises).then(() => { - setIsInitializing(false); - }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isInitializing]); - useEffect(() => { if (userConfirmedProxySetup) { const setup = async () => { @@ -438,7 +326,7 @@ const useChangeAction = ( }, [userConfirmedProxySetup]); const handleInitialize = () => { - setIsInitializing(true); + setOngoingAction(OngoingAction.Initializing); }; const handleSetup = () => { @@ -505,18 +393,6 @@ const useChangeAction = ( }; const getAction = () => { - // Check that more than 2/3 of the nodes are working, if not actions are - // disabled, since consensus cannot be achieved - const consensus = 2 / 3; - - if ( - Array.from(DKGStatuses.values()).filter((nodeStatus) => nodeStatus !== NodeStatus.Unreachable) - .length <= - consensus * roster.length - ) { - return <NoActionAvailable />; - } - // Except for seeing the results, all actions at least require the users // to be logged in if (!isLogged && status !== Status.ResultAvailable) { diff --git a/web/frontend/src/types/node.ts b/web/frontend/src/types/node.ts index 69bd94105..acccd0ee2 100644 --- a/web/frontend/src/types/node.ts +++ b/web/frontend/src/types/node.ts @@ -21,4 +21,30 @@ interface NodeProxyAddress { Proxy: string; } +// InternalDKGInfo is used to internally provide the status of DKG on a node. +class InternalDKGInfo { + static fromStatus(status: NodeStatus): InternalDKGInfo { + return new InternalDKGInfo(status, undefined); + } + + static fromInfo(info: DKGInfo): InternalDKGInfo { + return new InternalDKGInfo(info.Status, info.Error); + } + + private constructor(private status: NodeStatus, private error: DKGInfo['Error']) {} + + getError(): string { + if (this.error === undefined || this.error.Title === '') { + return ''; + } + + return this.error.Title + ' - ' + this.error.Code + ' - ' + this.error.Message; + } + + getStatus(): NodeStatus { + return this.status; + } +} + export type { DKGInfo, NodeProxyAddress }; +export { InternalDKGInfo }; From 958f7021816c25eae5eec47903accd715c890de3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9mien=20Kocher?= <nkcr.je@gmail.com> Date: Thu, 15 Sep 2022 12:02:56 +0200 Subject: [PATCH 03/10] Adds new DKG states and continues the refactoring --- services/dkg/mod.go | 8 ++ services/dkg/pedersen/handler.go | 11 ++- services/dkg/pedersen/mod.go | 14 +-- services/dkg/pedersen/mod_test.go | 2 +- .../src/components/utils/DKGStatus.tsx | 28 ++++++ web/frontend/src/pages/election/Show.tsx | 13 ++- .../election/components/DKGStatusRow.tsx | 92 +++++++++---------- .../election/components/DKGStatusTable.tsx | 17 +--- .../election/components/utils/PollStatus.ts | 9 +- web/frontend/src/types/node.ts | 8 ++ 10 files changed, 127 insertions(+), 75 deletions(-) diff --git a/services/dkg/mod.go b/services/dkg/mod.go index 54f750709..fbe43058b 100644 --- a/services/dkg/mod.go +++ b/services/dkg/mod.go @@ -24,6 +24,14 @@ const ( Setup StatusCode = 1 // Failed is when the actor failed to set up Failed StatusCode = 2 + // Dealing is when the actor is sending its deals + Dealing = 3 + // Responding is when the actor sends its responses on the deals + Responding = 4 + // Certifying is when the actor is validating its responses + Certifying = 5 + // Certified is then the actor is certified + Certified = 6 ) // DKG defines the primitive to start a DKG protocol diff --git a/services/dkg/pedersen/handler.go b/services/dkg/pedersen/handler.go index 925071f9f..e440de747 100644 --- a/services/dkg/pedersen/handler.go +++ b/services/dkg/pedersen/handler.go @@ -23,6 +23,7 @@ import ( etypes "github.com/dedis/d-voting/contracts/evoting/types" "github.com/dedis/d-voting/internal/testing/fake" + "github.com/dedis/d-voting/services/dkg" "github.com/dedis/d-voting/services/dkg/pedersen/types" "go.dedis.ch/dela" "go.dedis.ch/dela/core/ordering" @@ -70,12 +71,14 @@ type Handler struct { log zerolog.Logger running bool + + status *dkg.Status } // NewHandler creates a new handler func NewHandler(me mino.Address, service ordering.Service, pool pool.Pool, txnmngr txn.Manager, pubSharesSigner crypto.Signer, handlerData HandlerData, - context serde.Context, electionFac serde.Factory) *Handler { + context serde.Context, electionFac serde.Factory, status *dkg.Status) *Handler { privKey := handlerData.PrivKey pubKey := handlerData.PubKey @@ -101,6 +104,8 @@ func NewHandler(me mino.Address, service ordering.Service, pool pool.Pool, log: log, running: false, + + status: status, } } @@ -255,12 +260,15 @@ func (h *Handler) start(start types.Start, deals, resps *list.List, from mino.Ad // doDKG calls the subsequent DKG steps func (h *Handler) doDKG(deals, resps *list.List, out mino.Sender, from mino.Address) { h.log.Info().Str("action", "deal").Msg("new state") + *h.status = dkg.Status{Status: dkg.Dealing} h.deal(out) h.log.Info().Str("action", "respond").Msg("new state") + *h.status = dkg.Status{Status: dkg.Responding} h.respond(deals, out) h.log.Info().Str("action", "certify").Msg("new state") + *h.status = dkg.Status{Status: dkg.Certifying} err := h.certify(resps, out) if err != nil { dela.Logger.Error().Msgf("failed to certify: %v", err) @@ -268,6 +276,7 @@ func (h *Handler) doDKG(deals, resps *list.List, out mino.Sender, from mino.Addr } h.log.Info().Str("action", "finalize").Msg("new state") + *h.status = dkg.Status{Status: dkg.Certified} // Send back the public DKG key distKey, err := h.dkg.DistKeyShare() diff --git a/services/dkg/pedersen/mod.go b/services/dkg/pedersen/mod.go index a69366d4e..6b914f0c5 100644 --- a/services/dkg/pedersen/mod.go +++ b/services/dkg/pedersen/mod.go @@ -118,9 +118,11 @@ func (s *Pedersen) NewActor(electionIDBuf []byte, pool pool.Pool, txmngr txn.Man ctx := jsonserde.NewContext() + status := &dkg.Status{Status: dkg.Initialized} + // link the actor to an RPC by the election ID h := NewHandler(s.mino.GetAddress(), s.service, pool, txmngr, s.signer, - handlerData, ctx, s.electionFac) + handlerData, ctx, s.electionFac, status) no := s.mino.WithSegment(electionID) rpc := mino.MustCreateRPC(no, RPC, h, s.factory) @@ -135,7 +137,7 @@ func (s *Pedersen) NewActor(electionIDBuf []byte, pool pool.Pool, txmngr txn.Man electionFac: s.electionFac, handler: h, electionID: electionID, - status: dkg.Status{Status: dkg.Initialized}, + status: status, log: log, } @@ -167,12 +169,12 @@ type Actor struct { electionFac serde.Factory handler *Handler electionID string - status dkg.Status + status *dkg.Status log zerolog.Logger } func (a *Actor) setErr(err error, args map[string]interface{}) { - a.status = dkg.Status{ + *a.status = dkg.Status{ Status: dkg.Failed, Err: err, Args: args, @@ -313,7 +315,7 @@ func (a *Actor) Setup() (kyber.Point, error) { a.log.Info().Msgf("ok for %s", addr.String()) } - a.status = dkg.Status{Status: dkg.Setup} + *a.status = dkg.Status{Status: dkg.Setup} evoting.PromElectionDkgStatus.WithLabelValues(a.electionID).Set(float64(dkg.Setup)) return dkgPubKeys[0], nil @@ -397,7 +399,7 @@ func (a *Actor) MarshalJSON() ([]byte, error) { // Status implements dkg.Actor func (a *Actor) Status() dkg.Status { - return a.status + return *a.status } func electionExists(service ordering.Service, electionIDBuf []byte) (ordering.Proof, bool) { diff --git a/services/dkg/pedersen/mod_test.go b/services/dkg/pedersen/mod_test.go index 542bc92ac..cf4b70a4d 100644 --- a/services/dkg/pedersen/mod_test.go +++ b/services/dkg/pedersen/mod_test.go @@ -184,7 +184,7 @@ func TestPedersen_InitNonEmptyMap(t *testing.T) { otherActor := Actor{ handler: NewHandler(fake.NewAddress(0), &fake.Service{}, &fake.Pool{}, - fake.Manager{}, fake.Signer{}, handlerData, serdecontext, electionFac), + fake.Manager{}, fake.Signer{}, handlerData, serdecontext, electionFac, nil), } requireActorsEqual(t, actor, &otherActor) diff --git a/web/frontend/src/components/utils/DKGStatus.tsx b/web/frontend/src/components/utils/DKGStatus.tsx index 0e6d1a6d0..980449028 100644 --- a/web/frontend/src/components/utils/DKGStatus.tsx +++ b/web/frontend/src/components/utils/DKGStatus.tsx @@ -49,6 +49,34 @@ const DKGStatus: FC<DKGStatusProps> = ({ status }) => { <div>{t('failed')}</div> </div> ); + case NodeStatus.Dealing: + return ( + <div className="flex items-center"> + <div className="block h-4 w-4 bg-blue-500 rounded-full mr-2"></div> + <div>{t('dealing')}</div> + </div> + ); + case NodeStatus.Responding: + return ( + <div className="flex items-center"> + <div className="block h-4 w-4 bg-blue-500 rounded-full mr-2"></div> + <div>{t('responding')}</div> + </div> + ); + case NodeStatus.Certifying: + return ( + <div className="flex items-center"> + <div className="block h-4 w-4 bg-blue-500 rounded-full mr-2"></div> + <div>{t('certifying')}</div> + </div> + ); + case NodeStatus.Certified: + return ( + <div className="flex items-center"> + <div className="block h-4 w-4 bg-green-500 rounded-full mr-2"></div> + <div>{t('certified')}</div> + </div> + ); default: return null; } diff --git a/web/frontend/src/pages/election/Show.tsx b/web/frontend/src/pages/election/Show.tsx index 5d7d671c0..3f9c045ec 100644 --- a/web/frontend/src/pages/election/Show.tsx +++ b/web/frontend/src/pages/election/Show.tsx @@ -51,6 +51,7 @@ const ElectionShow: FC = () => { const ongoingItem = 'ongoingAction' + electionID; const nodeToSetupItem = 'nodeToSetup' + electionID; + // called by a DKG row const notifyDKGState = (node: string, info: InternalDKGInfo) => { console.log('DKG node updated:', info); switch (info.getStatus()) { @@ -70,6 +71,13 @@ const ElectionShow: FC = () => { console.log('dkg statuses:', DKGStatuses); }; + // called by a DKG row + const notifyLoading = (node: string, l: boolean) => { + const newLoading = new Map(nodeLoading); + newLoading.set(node, l); + setNodeLoading(newLoading); + }; + // Fetch result when available after a status change useEffect(() => { if (status === Status.ResultAvailable && isResultAvailable) { @@ -264,17 +272,14 @@ const ElectionShow: FC = () => { <DKGStatusTable roster={roster} electionId={electionId} - loading={nodeLoading} - setLoading={setNodeLoading} nodeProxyAddresses={nodeProxyAddresses} setNodeProxyAddresses={setNodeProxyAddresses} - DKGStatuses={DKGStatuses} - setDKGStatuses={setDKGStatuses} setTextModalError={setTextModalError} setShowModalError={setShowModalError} ongoingAction={ongoingAction} notifyDKGState={notifyDKGState} nodeToSetup={nodeToSetup} + notifyLoading={notifyLoading} /> </div> </div> diff --git a/web/frontend/src/pages/election/components/DKGStatusRow.tsx b/web/frontend/src/pages/election/components/DKGStatusRow.tsx index 0819dbd9f..1db1c57e9 100644 --- a/web/frontend/src/pages/election/components/DKGStatusRow.tsx +++ b/web/frontend/src/pages/election/components/DKGStatusRow.tsx @@ -12,33 +12,29 @@ import { ExclamationCircleIcon } from '@heroicons/react/outline'; import { pollDKG } from './utils/PollStatus'; const POLLING_INTERVAL = 1000; -const MAX_ATTEMPTS = 10; +const MAX_ATTEMPTS = 20; type DKGStatusRowProps = { electionId: ID; node: string; index: number; - loading: Map<string, boolean>; - setLoading: (loading: Map<string, boolean>) => void; nodeProxyAddresses: Map<string, string>; setNodeProxyAddresses: (nodeProxy: Map<string, string>) => void; - DKGStatuses: Map<string, NodeStatus>; - setDKGStatuses: (DKFStatuses: Map<string, NodeStatus>) => void; setTextModalError: (error: string) => void; setShowModalError: (show: boolean) => void; // notify to start initialization ongoingAction: OngoingAction; // notify the parent of the new state notifyDKGState: (node: string, info: InternalDKGInfo) => void; + // contains the node/proxy address of the node to setup nodeToSetup: [string, string]; + notifyLoading: (node: string, loading: boolean) => void; }; const DKGStatusRow: FC<DKGStatusRowProps> = ({ electionId, node, // node is the node address, not the proxy index, - loading, - setLoading, nodeProxyAddresses, setNodeProxyAddresses, setTextModalError, @@ -46,6 +42,7 @@ const DKGStatusRow: FC<DKGStatusRowProps> = ({ ongoingAction, notifyDKGState, nodeToSetup, + notifyLoading, }) => { const { t } = useTranslation(); const [proxy, setProxy] = useState(null); @@ -73,6 +70,13 @@ const DKGStatusRow: FC<DKGStatusRowProps> = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [status]); + // Notify the parent when we are loading or not + useEffect(() => { + notifyLoading(node, DKGLoading); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [DKGLoading]); + + // send the initialization request const initializeNode = async () => { const req = { method: 'POST', @@ -92,67 +96,71 @@ const DKGStatusRow: FC<DKGStatusRowProps> = ({ } }; - // Signal the start of DKG initialization + // Initialize the node if the initialization is ongoing and we are in a + // legitimate status. useEffect(() => { if ( ongoingAction === OngoingAction.Initializing && (status === NodeStatus.NotInitialized || status === NodeStatus.Failed) ) { - // TODO: can be modified such that if the majority of the node are - // initialized than the election status can still be set to initialized - setDKGLoading(true); initializeNode() .then(() => { setStatus(NodeStatus.Initialized); }) - .catch((e) => { + .catch((e: Error) => { setInfo(e.toString()); setStatus(NodeStatus.Failed); + }) + .finally(() => { + setDKGLoading(false); }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [ongoingAction]); - const pollDKGStatus = (statusToMatch: NodeStatus) => { + const pollDKGStatus = (statusToMatch: NodeStatus): Promise<DKGInfo> => { const req = { method: 'GET', signal: signal, }; const match = (s: NodeStatus) => s === statusToMatch; + const statusUpdate = (s: NodeStatus) => setStatus(s); return pollDKG( endpoints.getDKGActors(proxy, electionId), req, match, POLLING_INTERVAL, - MAX_ATTEMPTS + MAX_ATTEMPTS, + statusUpdate ); }; + // Action taken when the setting up is triggered. useEffect(() => { - if ( - ongoingAction === OngoingAction.SettingUp && - nodeToSetup !== null && - nodeToSetup[0] === node - ) { - setDKGLoading(true); - pollDKGStatus(NodeStatus.Setup) - .then( - () => { - setStatus(NodeStatus.Setup); - }, - (reason: any) => { - setStatus(NodeStatus.Failed); - setInfo(reason.toString()); - } - ) - .catch((e) => { - console.log('error:', e); - }); + if (ongoingAction !== OngoingAction.SettingUp || nodeToSetup === null) { + return; + } + + setDKGLoading(true); + + let expectedStatus = NodeStatus.Certified; + if (nodeToSetup[0] === node) { + expectedStatus = NodeStatus.Setup; } + + pollDKGStatus(expectedStatus) + .then( + () => {}, + (e: Error) => { + setStatus(NodeStatus.Failed); + setInfo(e.toString()); + } + ) + .finally(() => setDKGLoading(false)); // eslint-disable-next-line react-hooks/exhaustive-deps }, [ongoingAction]); @@ -241,7 +249,7 @@ const DKGStatusRow: FC<DKGStatusRowProps> = ({ } let dataReceived = await response.json(); - console.log('data received:', dataReceived); + return InternalDKGInfo.fromInfo(dataReceived as DKGInfo); } catch (e) { let errorMessage = t('errorRetrievingNodes'); @@ -265,30 +273,20 @@ const DKGStatusRow: FC<DKGStatusRowProps> = ({ fetchDKGStatus().then((internalStatus) => { setStatus(internalStatus.getStatus()); setInfo(internalStatus.getError()); + setDKGLoading(false); }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [proxy, status]); - useEffect(() => { - // UseEffect prevents the race condition on setDKGStatuses - if (status !== null) { - setDKGLoading(false); - - const newLoading = new Map(loading); - newLoading.set(node, false); - setLoading(newLoading); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [status]); - return ( <tr key={node} className="bg-white border-b hover:bg-gray-50"> <td className="px-1.5 sm:px-6 py-4 font-medium text-gray-900 whitespace-nowrap truncate"> {t('node')} {index} ({node}) </td> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 flex flex-row"> - {!DKGLoading ? <DKGStatus status={status} /> : <IndigoSpinnerIcon />} + {DKGLoading && <IndigoSpinnerIcon />} + <DKGStatus status={status} /> <Modal textModal={info} buttonRightText="close" diff --git a/web/frontend/src/pages/election/components/DKGStatusTable.tsx b/web/frontend/src/pages/election/components/DKGStatusTable.tsx index 53d451480..6d28b0241 100644 --- a/web/frontend/src/pages/election/components/DKGStatusTable.tsx +++ b/web/frontend/src/pages/election/components/DKGStatusTable.tsx @@ -2,18 +2,14 @@ import { FC } from 'react'; import { useTranslation } from 'react-i18next'; import { ID } from 'types/configuration'; import { OngoingAction } from 'types/election'; -import { InternalDKGInfo, NodeStatus } from 'types/node'; +import { InternalDKGInfo } from 'types/node'; import DKGStatusRow from './DKGStatusRow'; type DKGStatusTableProps = { roster: string[]; electionId: ID; - loading: Map<string, boolean>; - setLoading: (loading: Map<string, boolean>) => void; nodeProxyAddresses: Map<string, string>; setNodeProxyAddresses: (nodeProxy: Map<string, string>) => void; - DKGStatuses: Map<string, NodeStatus>; - setDKGStatuses: (DKFStatuses: Map<string, NodeStatus>) => void; setTextModalError: (error: string) => void; setShowModalError: (show: boolean) => void; // notify to start initialization @@ -21,22 +17,20 @@ type DKGStatusTableProps = { // notify the parent of the new state notifyDKGState: (node: string, info: InternalDKGInfo) => void; nodeToSetup: [string, string]; + notifyLoading: (node: string, loading: boolean) => void; }; const DKGStatusTable: FC<DKGStatusTableProps> = ({ roster, electionId, - loading, - setLoading, nodeProxyAddresses, setNodeProxyAddresses, - DKGStatuses, - setDKGStatuses, setTextModalError, setShowModalError, ongoingAction, notifyDKGState, nodeToSetup, + notifyLoading, }) => { const { t } = useTranslation(); @@ -62,17 +56,14 @@ const DKGStatusTable: FC<DKGStatusTableProps> = ({ electionId={electionId} node={node} index={index} - loading={loading} - setLoading={setLoading} nodeProxyAddresses={nodeProxyAddresses} setNodeProxyAddresses={setNodeProxyAddresses} - DKGStatuses={DKGStatuses} - setDKGStatuses={setDKGStatuses} setTextModalError={setTextModalError} setShowModalError={setShowModalError} ongoingAction={ongoingAction} notifyDKGState={notifyDKGState} nodeToSetup={nodeToSetup} + notifyLoading={notifyLoading} /> ))} </tbody> diff --git a/web/frontend/src/pages/election/components/utils/PollStatus.ts b/web/frontend/src/pages/election/components/utils/PollStatus.ts index 243c4fde5..4157e7ca4 100644 --- a/web/frontend/src/pages/election/components/utils/PollStatus.ts +++ b/web/frontend/src/pages/election/components/utils/PollStatus.ts @@ -44,8 +44,9 @@ const pollDKG = ( request: RequestInit, validate: (status: NodeStatus) => boolean, interval: number, - maxAttempts: number -) => { + maxAttempts: number, + status: (status: NodeStatus) => void +): Promise<DKGInfo> => { let attempts = 0; const executePoll = async (resolve, reject) => { @@ -68,6 +69,8 @@ const pollDKG = ( throw new Error(JSON.stringify(result)); } + status(result.Status); + if (validate(result.Status)) { return resolve(result); } @@ -94,7 +97,7 @@ const pollDKG = ( } }; - return new Promise(executePoll); + return new Promise<DKGInfo>(executePoll); }; export { pollElection, pollDKG }; diff --git a/web/frontend/src/types/node.ts b/web/frontend/src/types/node.ts index acccd0ee2..100d6efcd 100644 --- a/web/frontend/src/types/node.ts +++ b/web/frontend/src/types/node.ts @@ -9,6 +9,14 @@ export const enum NodeStatus { Setup, // Failed is when the actor failed to set up Failed, + // Dealing is when the actor is sending its deals + Dealing, + // Responding is when the actor is sending its responses on the received deals + Responding, + // Certifying is when the actor is validating its deals based on the responses + Certifying, + // Certified is when the actor has been certified + Certified, } interface DKGInfo { From e7b7d1ef243db4b70c2330751b239847d9cb6aa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9mien=20Kocher?= <nkcr.je@gmail.com> Date: Thu, 15 Sep 2022 13:25:23 +0200 Subject: [PATCH 04/10] Updates the mock --- web/frontend/src/mocks/handlers.ts | 50 ++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/web/frontend/src/mocks/handlers.ts b/web/frontend/src/mocks/handlers.ts index 5a06a4a67..c7d3126ed 100644 --- a/web/frontend/src/mocks/handlers.ts +++ b/web/frontend/src/mocks/handlers.ts @@ -263,16 +263,60 @@ export const handlers = [ const newDKGStatus = new Map(mockDKG.get(ElectionID as string)); let node = ''; - mockElections.get(ElectionID as string).Roster.forEach((n) => { + const roster = mockElections.get(ElectionID as string).Roster; + + const INCREMENT = 1200; + + roster.forEach((n) => { const p = mockNodeProxyAddresses.get(n); if (p === body.Proxy) { node = n; } }); - newDKGStatus.set(node, NodeStatus.Setup); + const setup = () => { + newDKGStatus.set(node, NodeStatus.Setup); + mockDKG.set(ElectionID as string, newDKGStatus); + }; + + const certified = () => { + roster.forEach((n) => { + newDKGStatus.set(n, NodeStatus.Certified); + }); + mockDKG.set(ElectionID as string, newDKGStatus); + + setTimeout(setup, INCREMENT); + }; + + const certifying = () => { + roster.forEach((n) => { + newDKGStatus.set(n, NodeStatus.Certifying); + }); + mockDKG.set(ElectionID as string, newDKGStatus); + + setTimeout(certified, INCREMENT); + }; + + const responding = () => { + roster.forEach((n) => { + newDKGStatus.set(n, NodeStatus.Responding); + }); + mockDKG.set(ElectionID as string, newDKGStatus); + + setTimeout(certifying, INCREMENT); + }; + + const dealing = () => { + roster.forEach((n) => { + newDKGStatus.set(n, NodeStatus.Dealing); + }); + mockDKG.set(ElectionID as string, newDKGStatus); + + setTimeout(responding, INCREMENT); + }; + + setTimeout(dealing, INCREMENT); - setTimeout(() => mockDKG.set(ElectionID as string, newDKGStatus), SETUP_TIMER); break; case Action.BeginDecryption: setTimeout( From 930019fe8aae66d57ebc2c0c5c55fd7655c0a5db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9mien=20Kocher?= <nkcr.je@gmail.com> Date: Fri, 16 Sep 2022 10:05:49 +0200 Subject: [PATCH 05/10] Fixes some edge cases --- web/frontend/src/pages/election/Show.tsx | 26 +++++++------------ .../election/components/DKGStatusRow.tsx | 5 +++- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/web/frontend/src/pages/election/Show.tsx b/web/frontend/src/pages/election/Show.tsx index 3f9c045ec..688391d81 100644 --- a/web/frontend/src/pages/election/Show.tsx +++ b/web/frontend/src/pages/election/Show.tsx @@ -53,10 +53,8 @@ const ElectionShow: FC = () => { // called by a DKG row const notifyDKGState = (node: string, info: InternalDKGInfo) => { - console.log('DKG node updated:', info); switch (info.getStatus()) { case NodeStatus.Failed: - console.log('DKG node failed'); setOngoingAction(OngoingAction.None); break; case NodeStatus.Setup: @@ -68,7 +66,6 @@ const ElectionShow: FC = () => { const newDKGStatuses = new Map(DKGStatuses); newDKGStatuses.set(node, info.getStatus()); setDKGStatuses(newDKGStatuses); - console.log('dkg statuses:', DKGStatuses); }; // called by a DKG row @@ -103,7 +100,6 @@ const ElectionShow: FC = () => { const storedOngoingAction = JSON.parse(window.localStorage.getItem(ongoingItem)); if (storedOngoingAction !== null) { - console.log('stored ongoing action:', storedOngoingAction); setOngoingAction(storedOngoingAction); } @@ -152,10 +148,14 @@ const ElectionShow: FC = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [roster]); + // Keep the "DKGLoading" state according to "nodeLoading". This state tells if + // one of the element on the map is true. useEffect(() => { if (nodeLoading !== null) { if (!Array.from(nodeLoading.values()).includes(true)) { setDKGLoading(false); + } else { + setDKGLoading(true); } } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -166,24 +166,18 @@ const ElectionShow: FC = () => { if (status === Status.Initial) { if (DKGStatuses !== null && !DKGLoading) { const statuses = Array.from(DKGStatuses.values()); + setOngoingAction(OngoingAction.None); // TODO: can be modified such that if the majority of the node are // initialized than the election status can still be set to initialized - if (statuses.includes(NodeStatus.NotInitialized)) { - setOngoingAction(OngoingAction.None); - setStatus(Status.Initial); + if ( + statuses.includes(NodeStatus.NotInitialized) || + statuses.includes(NodeStatus.Unreachable) || + statuses.includes(NodeStatus.Failed) + ) { return; } - if (statuses.includes(NodeStatus.Setup)) { - setOngoingAction(OngoingAction.None); - setStatus(Status.Setup); - return; - } - - if (statuses.includes(NodeStatus.Unreachable)) return; - if (statuses.includes(NodeStatus.Failed)) return; - setStatus(Status.Initialized); // Status Failed is handled by useChangeAction diff --git a/web/frontend/src/pages/election/components/DKGStatusRow.tsx b/web/frontend/src/pages/election/components/DKGStatusRow.tsx index 1db1c57e9..3345c2aad 100644 --- a/web/frontend/src/pages/election/components/DKGStatusRow.tsx +++ b/web/frontend/src/pages/election/components/DKGStatusRow.tsx @@ -101,12 +101,15 @@ const DKGStatusRow: FC<DKGStatusRowProps> = ({ useEffect(() => { if ( ongoingAction === OngoingAction.Initializing && - (status === NodeStatus.NotInitialized || status === NodeStatus.Failed) + (status === NodeStatus.NotInitialized || + status === NodeStatus.Failed || + status === NodeStatus.Unreachable) ) { setDKGLoading(true); initializeNode() .then(() => { + setInfo(''); setStatus(NodeStatus.Initialized); }) .catch((e: Error) => { From 6c7f680724774f346f9eec9cd3f5bff7da05854f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9mien=20Kocher?= <nkcr.je@gmail.com> Date: Fri, 16 Sep 2022 13:30:56 +0200 Subject: [PATCH 06/10] Adds missing translation --- web/frontend/src/language/en.json | 4 ++++ web/frontend/src/mocks/handlers.ts | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/web/frontend/src/language/en.json b/web/frontend/src/language/en.json index f8d149563..112e499f1 100644 --- a/web/frontend/src/language/en.json +++ b/web/frontend/src/language/en.json @@ -132,6 +132,10 @@ "setupNode": "Setup Node", "statusOpen": "Open", "failed": "Failed", + "dealing": "Dealing", + "responding": "Responding", + "certifying": "Certifying", + "certified": "Certified", "opening": "Opening...", "statusClose": "Closed", "closing": "Closing...", diff --git a/web/frontend/src/mocks/handlers.ts b/web/frontend/src/mocks/handlers.ts index c7d3126ed..c2f3bf1f7 100644 --- a/web/frontend/src/mocks/handlers.ts +++ b/web/frontend/src/mocks/handlers.ts @@ -39,7 +39,6 @@ let mockUserDB = setupMockUserDB(); const RESPONSE_TIME = 500; const CHANGE_STATUS_TIMER = 2000; const INIT_TIMER = 1000; -const SETUP_TIMER = 2000; const SHUFFLE_TIMER = 2000; const DECRYPT_TIMER = 1000; From 3e3105dfcd113905485127fd37d85680043bf01b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9mien=20Kocher?= <nkcr.je@gmail.com> Date: Tue, 20 Sep 2022 12:25:08 +0200 Subject: [PATCH 07/10] Fixes the case where a node is empty \+ other fixes --- web/frontend/src/pages/election/Show.tsx | 13 +- .../election/components/DKGStatusRow.tsx | 117 ++++++++++++------ 2 files changed, 84 insertions(+), 46 deletions(-) diff --git a/web/frontend/src/pages/election/Show.tsx b/web/frontend/src/pages/election/Show.tsx index 688391d81..78fe9a8e0 100644 --- a/web/frontend/src/pages/election/Show.tsx +++ b/web/frontend/src/pages/election/Show.tsx @@ -152,11 +152,8 @@ const ElectionShow: FC = () => { // one of the element on the map is true. useEffect(() => { if (nodeLoading !== null) { - if (!Array.from(nodeLoading.values()).includes(true)) { - setDKGLoading(false); - } else { - setDKGLoading(true); - } + const someNodeLoading = Array.from(nodeLoading.values()).includes(true); + setDKGLoading(someNodeLoading); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [nodeLoading]); @@ -166,6 +163,12 @@ const ElectionShow: FC = () => { if (status === Status.Initial) { if (DKGStatuses !== null && !DKGLoading) { const statuses = Array.from(DKGStatuses.values()); + + // We want to update only if all nodes have already set their status + if (statuses.length !== roster.length) { + return; + } + setOngoingAction(OngoingAction.None); // TODO: can be modified such that if the majority of the node are diff --git a/web/frontend/src/pages/election/components/DKGStatusRow.tsx b/web/frontend/src/pages/election/components/DKGStatusRow.tsx index 3345c2aad..6e7e11fe9 100644 --- a/web/frontend/src/pages/election/components/DKGStatusRow.tsx +++ b/web/frontend/src/pages/election/components/DKGStatusRow.tsx @@ -70,12 +70,6 @@ const DKGStatusRow: FC<DKGStatusRowProps> = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [status]); - // Notify the parent when we are loading or not - useEffect(() => { - notifyLoading(node, DKGLoading); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [DKGLoading]); - // send the initialization request const initializeNode = async () => { const req = { @@ -106,19 +100,6 @@ const DKGStatusRow: FC<DKGStatusRowProps> = ({ status === NodeStatus.Unreachable) ) { setDKGLoading(true); - - initializeNode() - .then(() => { - setInfo(''); - setStatus(NodeStatus.Initialized); - }) - .catch((e: Error) => { - setInfo(e.toString()); - setStatus(NodeStatus.Failed); - }) - .finally(() => { - setDKGLoading(false); - }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [ongoingAction]); @@ -142,6 +123,68 @@ const DKGStatusRow: FC<DKGStatusRowProps> = ({ ); }; + // performAction does the initialization or the setup if appropriate + const performAction = () => { + // Initialize ? + if ( + ongoingAction === OngoingAction.Initializing && + (status === NodeStatus.NotInitialized || + status === NodeStatus.Failed || + status === NodeStatus.Unreachable) + ) { + if (proxy === '') { + setStatus(NodeStatus.Unreachable); + setInfo('proxy empty'); + setDKGLoading(false); + return; + } + + initializeNode() + .then(() => { + setInfo(''); + setStatus(NodeStatus.Initialized); + }) + .catch((e: Error) => { + setInfo(e.toString()); + setStatus(NodeStatus.Failed); + }) + .finally(() => { + setDKGLoading(false); + }); + + return; + } + + // Setup ? + if (ongoingAction === OngoingAction.SettingUp && nodeToSetup !== null) { + let expectedStatus = NodeStatus.Certified; + if (nodeToSetup[0] === node) { + expectedStatus = NodeStatus.Setup; + } + + pollDKGStatus(expectedStatus) + .then( + () => {}, + (e: Error) => { + setStatus(NodeStatus.Failed); + setInfo(e.toString()); + } + ) + .finally(() => setDKGLoading(false)); + } + }; + + // Notify the parent when we are loading or not + useEffect(() => { + notifyLoading(node, DKGLoading); + + if (DKGLoading) { + performAction(); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [DKGLoading]); + // Action taken when the setting up is triggered. useEffect(() => { if (ongoingAction !== OngoingAction.SettingUp || nodeToSetup === null) { @@ -150,20 +193,6 @@ const DKGStatusRow: FC<DKGStatusRowProps> = ({ setDKGLoading(true); - let expectedStatus = NodeStatus.Certified; - if (nodeToSetup[0] === node) { - expectedStatus = NodeStatus.Setup; - } - - pollDKGStatus(expectedStatus) - .then( - () => {}, - (e: Error) => { - setStatus(NodeStatus.Failed); - setInfo(e.toString()); - } - ) - .finally(() => setDKGLoading(false)); // eslint-disable-next-line react-hooks/exhaustive-deps }, [ongoingAction]); @@ -196,8 +225,8 @@ const DKGStatusRow: FC<DKGStatusRowProps> = ({ errorMessage += t('error'); } - setTextModalError(errorMessage + e.message); - setShowModalError(true); + setInfo(errorMessage + e.message); + setStatus(NodeStatus.Unreachable); // if we could not retrieve the proxy still resolve the promise // so that promise.then() goes to onSuccess() but display the error @@ -207,9 +236,13 @@ const DKGStatusRow: FC<DKGStatusRowProps> = ({ setDKGLoading(true); - fetchNodeProxy().then((nodeProxyAddress) => { - setProxy(nodeProxyAddress.Proxy); - }); + fetchNodeProxy() + .then((nodeProxyAddress) => { + setProxy(nodeProxyAddress.Proxy); + }) + .finally(() => { + setDKGLoading(false); + }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [node, proxy]); @@ -264,12 +297,14 @@ const DKGStatusRow: FC<DKGStatusRowProps> = ({ errorMessage += t('error'); } - setTextModalError(errorMessage + e.message); - setShowModalError(true); + setInfo(errorMessage + e.message); // if we could not retrieve the proxy still resolve the promise // so that promise.then() goes to onSuccess() but display the error - return InternalDKGInfo.fromStatus(NodeStatus.Unreachable); + return InternalDKGInfo.fromInfo({ + Status: NodeStatus.Failed, + Error: { Title: errorMessage, Message: e.message, Code: 0, Args: undefined }, + }); } }; From e40e1ea940a62aeba6b55b22219fc846fe3f246c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9mien=20Kocher?= <nkcr.je@gmail.com> Date: Tue, 20 Sep 2022 13:35:16 +0200 Subject: [PATCH 08/10] Adds build info on the footer --- web/frontend/src/layout/Footer.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/web/frontend/src/layout/Footer.tsx b/web/frontend/src/layout/Footer.tsx index f9bd12a14..463288314 100644 --- a/web/frontend/src/layout/Footer.tsx +++ b/web/frontend/src/layout/Footer.tsx @@ -21,6 +21,12 @@ const Footer = () => ( <ProxyInput /> </div> </div> + <div className="text-center"> + version: + {process.env.REACT_APP_VERSION || 'unknown'} - build{' '} + {process.env.REACT_APP_BUILD || 'unknown'} - on{' '} + {process.env.REACT_APP_BUILD_TIME || 'unknown'} + </div> </footer> </div> ); From 5e97a5f9efdc329f2a442474af2558925b76fca5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9mien=20Kocher?= <nkcr.je@gmail.com> Date: Wed, 21 Sep 2022 15:06:17 +0200 Subject: [PATCH 09/10] Fixes the setup state --- web/frontend/src/pages/election/Show.tsx | 26 ++++++++----------- .../election/components/DKGStatusRow.tsx | 10 +++---- .../election/components/DKGStatusTable.tsx | 6 ----- 3 files changed, 16 insertions(+), 26 deletions(-) diff --git a/web/frontend/src/pages/election/Show.tsx b/web/frontend/src/pages/election/Show.tsx index 78fe9a8e0..203151c56 100644 --- a/web/frontend/src/pages/election/Show.tsx +++ b/web/frontend/src/pages/election/Show.tsx @@ -53,14 +53,11 @@ const ElectionShow: FC = () => { // called by a DKG row const notifyDKGState = (node: string, info: InternalDKGInfo) => { - switch (info.getStatus()) { - case NodeStatus.Failed: - setOngoingAction(OngoingAction.None); - break; - case NodeStatus.Setup: - setOngoingAction(OngoingAction.None); - setStatus(Status.Setup); - break; + if ( + info.getStatus() === NodeStatus.Setup && + (status === Status.Initial || status === Status.Initialized) + ) { + setStatus(Status.Setup); } const newDKGStatuses = new Map(DKGStatuses); @@ -154,6 +151,9 @@ const ElectionShow: FC = () => { if (nodeLoading !== null) { const someNodeLoading = Array.from(nodeLoading.values()).includes(true); setDKGLoading(someNodeLoading); + if (!someNodeLoading) { + setOngoingAction(OngoingAction.None); + } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [nodeLoading]); @@ -169,8 +169,6 @@ const ElectionShow: FC = () => { return; } - setOngoingAction(OngoingAction.None); - // TODO: can be modified such that if the majority of the node are // initialized than the election status can still be set to initialized if ( @@ -222,12 +220,12 @@ const ElectionShow: FC = () => { )} <div className="py-6 pl-2"> <div className="font-bold uppercase text-lg text-gray-700">{t('status')}</div> - {DKGLoading && ( + {DKGLoading && ongoingAction === OngoingAction.None && ( <div className="px-2 pt-6"> <LoadingButton>{t('statusLoading')}</LoadingButton> </div> )} - {!DKGLoading && ( + {(!DKGLoading || ongoingAction !== OngoingAction.None) && ( <div className="px-2 pt-6 flex justify-center"> <StatusTimeline status={status} ongoingAction={ongoingAction} /> </div> @@ -237,7 +235,7 @@ const ElectionShow: FC = () => { <div className="font-bold uppercase text-lg text-gray-700 pb-2">{t('action')}</div> <div className="px-2"> {DKGLoading && <LoadingButton>{t('actionLoading')}</LoadingButton>}{' '} - {!DKGLoading && ( + {(!DKGLoading || ongoingAction !== OngoingAction.None) && ( <Action status={status} electionID={electionID} @@ -271,8 +269,6 @@ const ElectionShow: FC = () => { electionId={electionId} nodeProxyAddresses={nodeProxyAddresses} setNodeProxyAddresses={setNodeProxyAddresses} - setTextModalError={setTextModalError} - setShowModalError={setShowModalError} ongoingAction={ongoingAction} notifyDKGState={notifyDKGState} nodeToSetup={nodeToSetup} diff --git a/web/frontend/src/pages/election/components/DKGStatusRow.tsx b/web/frontend/src/pages/election/components/DKGStatusRow.tsx index 6e7e11fe9..d42a0dfd1 100644 --- a/web/frontend/src/pages/election/components/DKGStatusRow.tsx +++ b/web/frontend/src/pages/election/components/DKGStatusRow.tsx @@ -20,8 +20,6 @@ type DKGStatusRowProps = { index: number; nodeProxyAddresses: Map<string, string>; setNodeProxyAddresses: (nodeProxy: Map<string, string>) => void; - setTextModalError: (error: string) => void; - setShowModalError: (show: boolean) => void; // notify to start initialization ongoingAction: OngoingAction; // notify the parent of the new state @@ -37,8 +35,6 @@ const DKGStatusRow: FC<DKGStatusRowProps> = ({ index, nodeProxyAddresses, setNodeProxyAddresses, - setTextModalError, - setShowModalError, ongoingAction, notifyDKGState, nodeToSetup, @@ -170,12 +166,16 @@ const DKGStatusRow: FC<DKGStatusRowProps> = ({ setInfo(e.toString()); } ) - .finally(() => setDKGLoading(false)); + .finally(() => { + console.log('setDKGLoading to false'); + setDKGLoading(false); + }); } }; // Notify the parent when we are loading or not useEffect(() => { + console.log('notifyLoading', DKGLoading); notifyLoading(node, DKGLoading); if (DKGLoading) { diff --git a/web/frontend/src/pages/election/components/DKGStatusTable.tsx b/web/frontend/src/pages/election/components/DKGStatusTable.tsx index 6d28b0241..02ae64618 100644 --- a/web/frontend/src/pages/election/components/DKGStatusTable.tsx +++ b/web/frontend/src/pages/election/components/DKGStatusTable.tsx @@ -10,8 +10,6 @@ type DKGStatusTableProps = { electionId: ID; nodeProxyAddresses: Map<string, string>; setNodeProxyAddresses: (nodeProxy: Map<string, string>) => void; - setTextModalError: (error: string) => void; - setShowModalError: (show: boolean) => void; // notify to start initialization ongoingAction: OngoingAction; // notify the parent of the new state @@ -25,8 +23,6 @@ const DKGStatusTable: FC<DKGStatusTableProps> = ({ electionId, nodeProxyAddresses, setNodeProxyAddresses, - setTextModalError, - setShowModalError, ongoingAction, notifyDKGState, nodeToSetup, @@ -58,8 +54,6 @@ const DKGStatusTable: FC<DKGStatusTableProps> = ({ index={index} nodeProxyAddresses={nodeProxyAddresses} setNodeProxyAddresses={setNodeProxyAddresses} - setTextModalError={setTextModalError} - setShowModalError={setShowModalError} ongoingAction={ongoingAction} notifyDKGState={notifyDKGState} nodeToSetup={nodeToSetup} From 3a6ab2065329f95e7aa4809a98fe2d304f61adb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=A9mien=20Kocher?= <nkcr.je@gmail.com> Date: Wed, 21 Sep 2022 17:42:19 +0200 Subject: [PATCH 10/10] Adds missing condition while loading --- web/frontend/src/pages/election/Show.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/frontend/src/pages/election/Show.tsx b/web/frontend/src/pages/election/Show.tsx index 203151c56..c946db97a 100644 --- a/web/frontend/src/pages/election/Show.tsx +++ b/web/frontend/src/pages/election/Show.tsx @@ -234,7 +234,9 @@ const ElectionShow: FC = () => { <div className="py-4 pl-2 pb-8"> <div className="font-bold uppercase text-lg text-gray-700 pb-2">{t('action')}</div> <div className="px-2"> - {DKGLoading && <LoadingButton>{t('actionLoading')}</LoadingButton>}{' '} + {DKGLoading && ongoingAction === OngoingAction.None && ( + <LoadingButton>{t('actionLoading')}</LoadingButton> + )}{' '} {(!DKGLoading || ongoingAction !== OngoingAction.None) && ( <Action status={status}