Skip to content

Commit

Permalink
feat: add nomination
Browse files Browse the repository at this point in the history
  • Loading branch information
hejkerooo committed Feb 18, 2025
1 parent 4976302 commit c41ff32
Show file tree
Hide file tree
Showing 31 changed files with 435 additions and 715 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ yarn-debug.log*
yarn-error.log*
node_modules/
*.tsbuildinfo
dist
dist
*.tgz
11 changes: 10 additions & 1 deletion .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,13 @@
!package.json
!yarn.lock
!submit-solution.js
!submit-solution.html
!submit-solution.html
!energywebx-config.js
!energywebx-config.html
!is-nominated.html
!is-nominated.js
!consensus.html
!consensus.js
!icons/consensus.svg
!icons/nomination.svg
!icons/submit-result.svg
23 changes: 23 additions & 0 deletions consensus.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<script type="text/javascript">
RED.nodes.registerType('consensus', {
category: 'Energy Web X',
paletteLabel: 'Offline Consensus',
color: '#60A0FF',
inputs: 1,
outputs: 1,
icon: "icons/consensus.svg",
label: function() {
return this.name || "Offline Consensus";
},
inputLabels: [],
outputLabels: [],
align: 'left',
defaults: {
ewxConfig: { value:"", type: "energywebx-config", required: true },
}
});
</script>

<script type="text/html" data-template-name="consensus">
<p>Checks if consensus was reached for given voting round.</p>
</script>
233 changes: 233 additions & 0 deletions consensus.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
module.exports = function (RED) {
const axios = require('axios');

const ConsensusStatus = {
NOT_ENOUGH_VOTES: 'NOT_ENOUGH_VOTES',
REACHED: 'REACHED',
UNABLE_TO_REACH_CONSENSUS: 'UNABLE_TO_REACH_CONSENSUS',
FAILED: 'FAILED'
};

const gqlQuery = `
query GetSubmittedResults($solutionNamespace: String!, $votingRoundId: String!, $solutionGroupId: String!, $limit: Int!, $offset: Int!) {
solutionResultSubmitteds(where: {solution: {id_eq: $solutionNamespace}votingRoundId_eq: $votingRoundId, successful_eq: true}, orderBy: blockNumber_ASC, limit: $limit, offset: $offset) {
result
worker {
id
}
}
operatorSubscribedSolutionGroups(where: {solutionGroup: {id_eq: $solutionGroupId}}, limit: $limit, offset: $offset) {
operator {
id
mappings {
worker {
id
}
}
}
}
}
`;
const getKeyWithHighestNumber = (obj) =>

Object.keys(obj).reduce((a, b) => (obj[a] > obj[b] ? a : b));

function NodeConstructor(config) {
this.ewxConfig = RED.nodes.getNode(config.ewxConfig);

RED.nodes.createNode(this, config);

var node = this;

node.on('input', async function (msg, send, done) {
if (!msg.payload.votingRoundId) {
this.status({fill: "red", shape: "dot", text: "votingRoundId is missing"});

node.error("votingRoundId is missing");

return;
}

let electedLeader = null;

let page = 0;
const limit = 50;

const resultCounts = {};
const operatorsMapping = {};
const submittedResults = [];

while (true) {
const response = await axios.post(node.ewxConfig.subsquidUrl, {
query: gqlQuery,
variables: {
votingRoundId: msg.payload.votingRoundId,
solutionGroupId: node.ewxConfig.solutionGroupId,
solutionNamespace: node.ewxConfig.solutionNamespace,
limit,
offset: (page * limit)
}
}).catch((e) => {
console.error(`failed during fetching data, solution: ${node.ewxConfig.solutionNamespace}`, e, e.response?.data);
this.status({fill: "red", shape: "dot", text: "failed to query data"});

return null;
});

if (response === null) {
send({
payload: {
leaderAddress: null,
consensusStatus: ConsensusStatus.FAILED,
attempt: msg.payload.attempt ? msg.payload.attempt + 1 : 1,
shouldRetry: true,
result: null
}
});

done();

return;
}

const {data} = response;

const {solutionResultSubmitteds, operatorSubscribedSolutionGroups} = data.data;

if (solutionResultSubmitteds.length === 0 && operatorSubscribedSolutionGroups.length === 0) {
break;
}

submittedResults.push(...solutionResultSubmitteds);

for (const {operator} of operatorSubscribedSolutionGroups) {
if (operatorsMapping[operator.id]) {
continue;
}

const mappings = operator.mappings;

if (mappings.length === 0) {
continue;
}

operatorsMapping[operator.id] = mappings[0].worker.id;
}

page++;
}

this.log(`votingRoundId = ${msg.payload.votingRoundId} - finished fetching consensus data`);

const applicableOperatorsCount = Object.entries(operatorsMapping).length;

if (applicableOperatorsCount < 3) {
this.log(`votingRoundId = ${msg.payload.votingRoundId} - not enough operators for consensus`);
this.status({fill: "red", shape: "dot", text: "not enough operators"});

send({
payload: {
leaderAddress: null,
consensusStatus: ConsensusStatus.FAILED,
attempt: msg.payload.attempt ? msg.payload.attempt + 1 : 1,
shouldRetry: false,
result: null
}
});

return done();
}

const hasAnyVotes = submittedResults.length > 0;

if (!hasAnyVotes) {
this.log(`votingRoundId = ${msg.payload.votingRoundId} - not enough votes for consensus`);

this.status({fill: "yellow", shape: "dot", text: "not enough votes"});

send({
payload: {
leaderAddress: null,
consensusStatus: ConsensusStatus.NOT_ENOUGH_VOTES,
attempt: msg.payload.attempt ? msg.payload.attempt + 1 : 1,
shouldRetry: true,
result: null
}
});

return done();
}


const minVotesRequired = applicableOperatorsCount / 2 + 0.5;

for (const {result, worker} of submittedResults) {
resultCounts[result] = (resultCounts[result] || 0) + 1;

if (resultCounts[result] >= minVotesRequired && electedLeader == null) {
electedLeader = worker.id;
}
}

if (electedLeader) {
this.log(`votingRoundId = ${msg.payload.votingRoundId} - reached consensus`);

this.status({fill: "green", shape: "dot", text: "reached"});
const resultHash = getKeyWithHighestNumber(resultCounts);

send({
payload: {
leaderAddress: electedLeader,
consensusStatus: ConsensusStatus.REACHED,
attempt: msg.payload.attempt ? msg.payload.attempt + 1 : 1,
shouldRetry: false,
resultHash
}
});

return done();
}

const highestVote = resultCounts[getKeyWithHighestNumber(resultCounts)];

const remainingVotes = applicableOperatorsCount - submittedResults.length;

const canStillReachConsensus = highestVote + remainingVotes >= minVotesRequired;

if (!canStillReachConsensus) {
this.status({fill: "red", shape: "dot", text: "unable to reach"});
this.log(`votingRoundId = ${msg.payload.votingRoundId} - unable to reach consensus`);

send({
payload: {
leaderAddress: null,
consensusStatus: ConsensusStatus.UNABLE_TO_REACH_CONSENSUS,
attempt: msg.payload.attempt ? msg.payload.attempt + 1 : 1,
shouldRetry: false,
result: null
}
});

return done();
} else {
this.status({fill: "yellow", shape: "dot", text: "not enough votes"});
this.log(`votingRoundId = ${msg.payload.votingRoundId} - not enough votes`);


send({
payload: {
leaderAddress: null,
consensusStatus: ConsensusStatus.NOT_ENOUGH_VOTES,
attempt: msg.payload.attempt ? msg.payload.attempt + 1 : 1,
shouldRetry: true,
result: null
}
});

return done();
}
});
}

RED.nodes.registerType("consensus", NodeConstructor);
}
21 changes: 13 additions & 8 deletions energywebx-config.html
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
<script type="text/javascript">
RED.nodes.registerType('energywebx-config', {
category: 'config',
icon: "energywebx-logo.png",
label: function() {
return this.energywebxconfig;
color: '#60A0FF',
paletteLabel: 'Energy Web X Config',
label: function () {
return `${this.networkName.toUpperCase()} Config`;
},
defaults: {
networkName: { value: "ewx", required: true },
solutionNamespace: { value: "oep", required: true },
rpcUrl: { value: {} },
socketUrl: { value: {} },
explorerUrl: { value: {} }
solutionNamespace: {value: "", default: "", required: true},
solutionGroupId: {value: "", default: "", required: true},
rpcUrl: {value: null, required: true},
subsquidUrl: {value: null, required: true},
workerUrl: {value: "", default: 'http://localhost:3002', required: true},
workerAddress: {value: "", default: ""}
}
});
</script>

<script type="text/html" data-template-name="energywebx-config">
</script>
66 changes: 52 additions & 14 deletions energywebx-config.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,58 @@
module.exports = function(RED) {

const polkadot = require('@polkadot/api');

const URLS = {
PEX: 'https://public-rpc.testnet.energywebx.com',
MAINNET: 'https://public-rpc.mainnet.energywebx.com'
};

const matchRpcToSubsquid = (rpcUrl) => {
if (rpcUrl === URLS.PEX) {
return 'https://ewx-subsquid-dev.energyweb.org/graphql'
} else if (rpcUrl === URLS.MAINNET) {
return 'https://ewx-indexer.mainnet.energywebx.com/graphql';
}

return process.env.__EWX_SUBSQUID_URL;
}


module.exports = function (RED) {
function EnergyWebXConfigNode(config) {

RED.nodes.createNode(this, config);
this.solutionNamespace = config.solutionNamespace;

if (this.networkName === 'rex'){
this.rpcUrl = 'https://rex-rpc.energywebx.org/';
this.socketUrl = 'https://rex-rpc.energywebx.org/ws';
this.explorerUrl = 'https://rex-explorer.energywebx.org/api';
} else if (this.networkName === 'ewx') {
this.rpcUrl = 'https://rpc.energywebx.org/';
this.socketUrl = 'https://rpc.energywebx.org/ws';
this.explorerUrl = 'https://explorer.energywebx.org/api';
}

const ewxRemoteConfig = config.__envConfig;

this.workerUrl = 'http://localhost:3002';

this.workerAddress = ewxRemoteConfig.EWX_WORKER_ADDRESS;
this.solutionNamespace = ewxRemoteConfig.EWX_SOLUTION_ID;
this.solutionGroupId = ewxRemoteConfig.EWX_SOLUTION_GROUP_ID;
this.rpcUrl = ewxRemoteConfig.EWX_RPC_URL;
this.subsquidUrl = matchRpcToSubsquid(this.rpcUrl);

this.log(`worker address = ${this.workerAddress}, solution namespace = ${this.solutionNamespace}, solution group id = ${this.solutionGroupId}, rpc url = ${this.rpcUrl}, subsquid url = ${this.subsquidUrl}`)

const provider = new polkadot.HttpProvider(this.rpcUrl);

const api = new polkadot.ApiPromise({
provider,
throwOnUnknown: true,
throwOnConnect: true,
});

api.connect()
.then(() => {
this.log(`connected to ${this.rpcUrl}`);

this.status({fill: "green", shape: "dot", text: "connected"});
})
.catch((e) => {
this.log(e);

this.status({fill: "red", shape: "ring", text: "disconnected"});
})
}

RED.nodes.registerType("energywebx-config", EnergyWebXConfigNode);
}
Loading

0 comments on commit c41ff32

Please sign in to comment.