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 = ({ status }) => {
{t('failed')}
); + case NodeStatus.Dealing: + return ( +
+
+
{t('dealing')}
+
+ ); + case NodeStatus.Responding: + return ( +
+
+
{t('responding')}
+
+ ); + case NodeStatus.Certifying: + return ( +
+
+
{t('certifying')}
+
+ ); + case NodeStatus.Certified: + return ( +
+
+
{t('certified')}
+
+ ); default: return null; } 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/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 = () => ( +
+ version: + {process.env.REACT_APP_VERSION || 'unknown'} - build{' '} + {process.env.REACT_APP_BUILD || 'unknown'} - on{' '} + {process.env.REACT_APP_BUILD_TIME || 'unknown'} +
); diff --git a/web/frontend/src/mocks/handlers.ts b/web/frontend/src/mocks/handlers.ts index ca53c8b75..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; @@ -63,7 +62,7 @@ export const handlers = [ ? { lastname: 'Bobster', firstname: 'Alice', - role: UserRole.Voter, + role: UserRole.Admin, sciper: userId, } : {}; @@ -263,16 +262,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( diff --git a/web/frontend/src/pages/election/Show.tsx b/web/frontend/src/pages/election/Show.tsx index e88241497..c946db97a 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>(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>(new Map()); const [nodeLoading, setNodeLoading] = useState>(null); @@ -51,6 +51,27 @@ const ElectionShow: FC = () => { const ongoingItem = 'ongoingAction' + electionID; const nodeToSetupItem = 'nodeToSetup' + electionID; + // called by a DKG row + const notifyDKGState = (node: string, info: InternalDKGInfo) => { + if ( + info.getStatus() === NodeStatus.Setup && + (status === Status.Initial || status === Status.Initialized) + ) { + setStatus(Status.Setup); + } + + const newDKGStatuses = new Map(DKGStatuses); + newDKGStatuses.set(node, info.getStatus()); + setDKGStatuses(newDKGStatuses); + }; + + // 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) { @@ -124,10 +145,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); + const someNodeLoading = Array.from(nodeLoading.values()).includes(true); + setDKGLoading(someNodeLoading); + if (!someNodeLoading) { + setOngoingAction(OngoingAction.None); } } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -139,17 +164,21 @@ const ElectionShow: FC = () => { 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; + } + // 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.Setup)) { - setStatus(Status.Setup); + if ( + statuses.includes(NodeStatus.NotInitialized) || + statuses.includes(NodeStatus.Unreachable) || + statuses.includes(NodeStatus.Failed) + ) { return; } - if (statuses.includes(NodeStatus.Unreachable)) return; - setStatus(Status.Initialized); // Status Failed is handled by useChangeAction @@ -191,12 +220,12 @@ const ElectionShow: FC = () => { )}
{t('status')}
- {DKGLoading && ( + {DKGLoading && ongoingAction === OngoingAction.None && (
{t('statusLoading')}
)} - {!DKGLoading && ( + {(!DKGLoading || ongoingAction !== OngoingAction.None) && (
@@ -205,8 +234,10 @@ const ElectionShow: FC = () => {
{t('action')}
- {DKGLoading && {t('actionLoading')}}{' '} - {!DKGLoading && ( + {DKGLoading && ongoingAction === OngoingAction.None && ( + {t('actionLoading')} + )}{' '} + {(!DKGLoading || ongoingAction !== OngoingAction.None) && ( { setOngoingAction={setOngoingAction} nodeToSetup={nodeToSetup} setNodeToSetup={setNodeToSetup} - DKGStatuses={DKGStatuses} - setDKGStatuses={setDKGStatuses} /> )}
@@ -240,14 +269,12 @@ const ElectionShow: FC = () => {
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; - setDKGStatuses: (dkgStatuses: Map) => void; }; const Action: FC = ({ @@ -36,8 +33,6 @@ const Action: FC = ({ setOngoingAction, nodeToSetup, setNodeToSetup, - DKGStatuses, - setDKGStatuses, }) => { const { getAction, modalClose, modalCancel, modalDelete, modalSetup } = useChangeAction( status, @@ -51,9 +46,7 @@ const Action: FC = ({ 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; nodeProxyAddresses: Map; nodeToSetup: [string, string]; setNodeToSetup: (node: [string, string]) => void; @@ -18,7 +16,6 @@ type ChooseProxyModalProps = { const ChooseProxyModal: FC = ({ roster, showModal, - DKGStatuses, nodeProxyAddresses, nodeToSetup, setNodeToSetup, @@ -48,8 +45,7 @@ const ChooseProxyModal: FC = ({ 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 (
diff --git a/web/frontend/src/pages/election/components/DKGStatusRow.tsx b/web/frontend/src/pages/election/components/DKGStatusRow.tsx index 1f81eb185..d42a0dfd1 100644 --- a/web/frontend/src/pages/election/components/DKGStatusRow.tsx +++ b/web/frontend/src/pages/election/components/DKGStatusRow.tsx @@ -2,43 +2,51 @@ 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 = 20; type DKGStatusRowProps = { electionId: ID; node: string; index: number; - loading: Map; - setLoading: (loading: Map) => void; nodeProxyAddresses: Map; setNodeProxyAddresses: (nodeProxy: Map) => void; - DKGStatuses: Map; - setDKGStatuses: (DKFStatuses: Map) => 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 = ({ electionId, - node, + node, // node is the node address, not the proxy index, - loading, - setLoading, nodeProxyAddresses, setNodeProxyAddresses, - DKGStatuses, - setDKGStatuses, - setTextModalError, - setShowModalError, + ongoingAction, + notifyDKGState, + nodeToSetup, + notifyLoading, }) => { const { t } = useTranslation(); const [proxy, setProxy] = useState(null); const [DKGLoading, setDKGLoading] = useState(true); const [status, setStatus] = useState(null); + const [info, setInfo] = useState(''); + const abortController = new AbortController(); const signal = abortController.signal; const TIMEOUT = 10000; @@ -50,13 +58,143 @@ const DKGStatusRow: FC = ({ signal: signal, }; + const [showModal, setShowModal] = useState(false); + + // Notify the parent each time our status changes + useEffect(() => { + notifyDKGState(node, InternalDKGInfo.fromStatus(status)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [status]); + + // send the initialization request + 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); + } + }; + + // Initialize the node if the initialization is ongoing and we are in a + // legitimate status. useEffect(() => { - // update status on useChangeAction (i.e. initializing and setting up) - if (DKGStatuses.has(node)) { - setStatus(DKGStatuses.get(node)); + if ( + ongoingAction === OngoingAction.Initializing && + (status === NodeStatus.NotInitialized || + status === NodeStatus.Failed || + status === NodeStatus.Unreachable) + ) { + setDKGLoading(true); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [DKGStatuses]); + }, [ongoingAction]); + + const pollDKGStatus = (statusToMatch: NodeStatus): Promise => { + 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, + statusUpdate + ); + }; + + // 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(() => { + 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) { + 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) { + return; + } + + setDKGLoading(true); + + // 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) @@ -87,8 +225,8 @@ const DKGStatusRow: FC = ({ 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 @@ -96,9 +234,15 @@ const DKGStatusRow: FC = ({ } }; - fetchNodeProxy().then((nodeProxyAddress) => { - setProxy(nodeProxyAddress.Proxy); - }); + setDKGLoading(true); + + fetchNodeProxy() + .then((nodeProxyAddress) => { + setProxy(nodeProxyAddress.Proxy); + }) + .finally(() => { + setDKGLoading(false); + }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [node, proxy]); @@ -116,12 +260,12 @@ const DKGStatusRow: FC = ({ // Fetch the status of the nodes useEffect(() => { if (proxy !== null && status === null) { - const fetchDKGStatus = async () => { + const fetchDKGStatus = async (): Promise => { // 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 +276,7 @@ const DKGStatusRow: FC = ({ 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 +285,8 @@ const DKGStatusRow: FC = ({ } let dataReceived = await response.json(); - return dataReceived.Status as NodeStatus; + + return InternalDKGInfo.fromInfo(dataReceived as DKGInfo); } catch (e) { let errorMessage = t('errorRetrievingNodes'); @@ -152,46 +297,48 @@ const DKGStatusRow: FC = ({ 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 NodeStatus.Unreachable; + return InternalDKGInfo.fromInfo({ + Status: NodeStatus.Failed, + Error: { Title: errorMessage, Message: e.message, Code: 0, Args: undefined }, + }); } }; - fetchDKGStatus().then((nodeStatus) => { - setStatus(nodeStatus); + 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); - - // notify parent - const newDKGStatuses = new Map(DKGStatuses); - newDKGStatuses.set(node, status); - setDKGStatuses(newDKGStatuses); - - const newLoading = new Map(loading); - newLoading.set(node, false); - setLoading(newLoading); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [status]); - return ( {t('node')} {index} ({node}) - - {!DKGLoading ? : } + + {DKGLoading && } + + + {info !== '' && ( + + )} ); diff --git a/web/frontend/src/pages/election/components/DKGStatusTable.tsx b/web/frontend/src/pages/election/components/DKGStatusTable.tsx index 4214f264a..02ae64618 100644 --- a/web/frontend/src/pages/election/components/DKGStatusTable.tsx +++ b/web/frontend/src/pages/election/components/DKGStatusTable.tsx @@ -1,33 +1,32 @@ 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 } from 'types/node'; import DKGStatusRow from './DKGStatusRow'; type DKGStatusTableProps = { roster: string[]; electionId: ID; - loading: Map; - setLoading: (loading: Map) => void; nodeProxyAddresses: Map; setNodeProxyAddresses: (nodeProxy: Map) => void; - DKGStatuses: Map; - setDKGStatuses: (DKFStatuses: Map) => 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]; + notifyLoading: (node: string, loading: boolean) => void; }; const DKGStatusTable: FC = ({ roster, electionId, - loading, - setLoading, nodeProxyAddresses, setNodeProxyAddresses, - DKGStatuses, - setDKGStatuses, - setTextModalError, - setShowModalError, + ongoingAction, + notifyDKGState, + nodeToSetup, + notifyLoading, }) => { const { t } = useTranslation(); @@ -53,14 +52,12 @@ const DKGStatusTable: FC = ({ 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} /> ))} 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 => { 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(executePoll); }; export { pollElection, pollDKG }; diff --git a/web/frontend/src/pages/election/components/utils/useChangeAction.tsx b/web/frontend/src/pages/election/components/utils/useChangeAction.tsx index 7ccb4b669..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, - setDKGStatuses: (dkgStatuses: Map) => 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 = ( { - 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.values()).map((proxy) => { - if (proxy !== '') { - 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 ; - } - // 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..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 { @@ -21,4 +29,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 };