Description
Masternode identity metadata saved through the Governance Dashboard is initially
updated successfully, but it is later reset to the default values.
After saving the identity, the updated candidate name is visible publicly,
including in a separate incognito browser session. After some time, the candidate
name changes back to XDC.Network, while the hardware and data center fields are
shown as N/A.
The behavior appears to be caused by getCurrentCandidates() deleting and
recreating all candidate documents every 150 blocks and on application startup.
Environment
Steps to reproduce
- Open the Apothem Governance Dashboard.
- Log in with the candidate owner account.
- Open the registered candidate page.
- Click the edit button next to the candidate name.
- Set the candidate identity, for example:
- Name:
Cointelegraph (CTDG)
- Hardware
- Data Center Name
- Data Center Location
- Website
- Telegram
- Sign and submit the update.
- Open the candidate page in an incognito browser session.
- Verify that the new name is publicly visible.
- Wait for the next candidate refresh, or restart the backend.
- Reload the candidate page.
Actual result
The identity metadata disappears:
- Candidate name falls back to
XDC.Network
- Hardware becomes
N/A
- Data Center Name becomes
N/A
- Data Center Location becomes
N/A
- Website and Telegram are removed
The candidate itself remains registered and continues to display the correct
owner and coinbase addresses.
Expected result
Identity metadata saved by the authenticated candidate owner should remain
associated with the candidate across:
- Periodic blockchain synchronization
- Candidate capacity refreshes
- Backend restarts
Technical analysis
The update endpoint correctly stores the custom metadata using $set:
|
router.put('/update', [ |
|
check('name').isLength({ min: 3, max: 30 }).optional().withMessage('Name must be 3 - 30 chars long'), |
|
check('hardware').isLength({ min: 3, max: 30 }).optional().withMessage('Hardware must be 3 - 30 chars long'), |
|
check('dcName').isLength({ min: 2, max: 30 }).optional().withMessage('dcName must be 2 - 30 chars long'), |
|
check('dcLocation').isLength({ min: 2, max: 30 }).optional().withMessage('dcLocation must be 2 - 30 chars long'), |
|
check('signedMessage').isLength({ min: 1 }).exists().withMessage('signedMessage is required'), |
|
check('message').isLength({ min: 1 }).exists().withMessage('message is required') |
|
], async function (req, res, next) { |
|
const errors = validationResult(req) |
|
if (!errors.isEmpty()) { |
|
return next(errors.array()) |
|
} |
|
try { |
|
const { signedMessage, message } = req.body |
|
const candidate = (req.body.candidate || '').toLowerCase() |
|
const c = await db.Candidate.findOne({ |
|
smartContractAddress: config.get('blockchain.validatorAddress'), |
|
candidate: candidate |
|
}) |
|
if (!c) { |
|
return next(new Error('Not found')) |
|
} |
|
|
|
const body = req.body |
|
let set = _.pick(body, ['name', 'hardware']) |
|
|
|
if (body.dcName) { |
|
set['dataCenter.name'] = body.dcName |
|
} |
|
if (body.dcLocation) { |
|
set['dataCenter.location'] = body.dcLocation |
|
} |
|
|
|
if (body.website && !validateUrl(body.website)) { |
|
return next(new Error('Invalid website URL')) |
|
} |
|
if (body.telegram && !validateUrl(body.telegram)) { |
|
return next(new Error('Invalid telegram URL')) |
|
} |
|
|
|
set['socials.website'] = body.website || '' |
|
set['socials.telegram'] = body.telegram || '' |
|
|
|
let address = await web3.eth.accounts.recover(message, signedMessage) |
|
if (address.substring(0, 2) === '0x') { |
|
address = 'xdc' + address.substring(2) |
|
} |
|
console.log('address', address) |
|
|
|
if (address.toLowerCase() === c.owner.toLowerCase()) { |
|
if (c.name) { |
|
const currentBlockNumber = await web3.eth.getBlockNumber() |
|
const data = set |
|
data.candidate = candidate.toLowerCase() |
|
data.blockNumber = currentBlockNumber |
|
|
|
await db.History.updateOne({ |
|
candidate: candidate.toLowerCase(), blockNumber: currentBlockNumber |
|
}, { |
|
$set: data |
|
}, { upsert: true }) |
|
} |
|
await db.Candidate.updateOne({ |
|
smartContractAddress: config.get('blockchain.validatorAddress'), |
|
candidate: candidate.toLowerCase() |
|
}, { |
|
$set: set |
|
}) |
The fields written by the endpoint include:
name
hardware
dataCenter.name
dataCenter.location
socials.website
socials.telegram
However, getCurrentCandidates() reads the previous candidate documents and
then deletes the entire candidate collection:
|
// Get current candates |
|
async function getCurrentCandidates () { |
|
try { |
|
let candidates = [] |
|
try { |
|
candidates = await validator.methods.getCandidates().call() |
|
} catch (rpcErr) { |
|
logger.error('RPC Error in getCandidates %s', rpcErr.message) |
|
return |
|
} |
|
|
|
const prevCandidates = await db.Candidate.find({}) |
|
await db.Candidate.remove({}) |
|
let map = candidates.map(async (candidate) => { |
|
const storedDetails = prevCandidates.find((e) => e.candidate === candidate.replace('0x', 'xdc').toLowerCase()) |
|
|
|
const storedLatestSignedBlock = storedDetails?.latestSignedBlock || 0 |
|
const prevStatus = storedDetails?.status || null |
|
|
|
if (candidate.substring(0, 3) === 'xdc') { |
|
candidate = '0x' + candidate.substring(3) |
|
} |
|
|
|
candidate = (candidate || '').toLowerCase() |
|
|
|
let voters = [] |
|
try { |
|
voters = await validator.methods.getVoters(candidate).call() |
|
} catch (rpcErr) { |
|
logger.error('RPC Error in getVoters %s', rpcErr.message) |
|
} |
|
|
|
let m = voters.map(v => { |
|
v = (v || '').toLowerCase() |
|
return updateVoterCap(candidate, v) |
|
}) |
|
|
|
await Promise.all(m) |
|
return updateCandidateInfo(candidate, storedLatestSignedBlock, prevStatus) |
const prevCandidates = await db.Candidate.find({})
await db.Candidate.remove({})
Description
Masternode identity metadata saved through the Governance Dashboard is initially
updated successfully, but it is later reset to the default values.
After saving the identity, the updated candidate name is visible publicly,
including in a separate incognito browser session. After some time, the candidate
name changes back to
XDC.Network, while the hardware and data center fields areshown as
N/A.The behavior appears to be caused by
getCurrentCandidates()deleting andrecreating all candidate documents every 150 blocks and on application startup.
Environment
xdce7a19c8113414b2643a39a6be16843d38f6be923xdc9ae43d7047bd683322f778402ffad9a760c5ddd7https://master.apothem.network/candidate/xdce7a19c8113414b2643a39a6be16843d38f6be923
fc925b16578d28aa2283ee3144926c88fb26ae7eSteps to reproduce
Cointelegraph (CTDG)Actual result
The identity metadata disappears:
XDC.NetworkN/AN/AN/AThe candidate itself remains registered and continues to display the correct
owner and coinbase addresses.
Expected result
Identity metadata saved by the authenticated candidate owner should remain
associated with the candidate across:
Technical analysis
The update endpoint correctly stores the custom metadata using
$set:MasterNode-App/apis/candidates.js
Lines 793 to 860 in fc925b1
The fields written by the endpoint include:
namehardwaredataCenter.namedataCenter.locationsocials.websitesocials.telegramHowever,
getCurrentCandidates()reads the previous candidate documents andthen deletes the entire candidate collection:
MasterNode-App/crawl.js
Lines 269 to 307 in fc925b1