Skip to content

Masternode identity metadata is erased by the candidate refresh every 150 blocks #66

Description

@loglapa

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

  1. Open the Apothem Governance Dashboard.
  2. Log in with the candidate owner account.
  3. Open the registered candidate page.
  4. Click the edit button next to the candidate name.
  5. Set the candidate identity, for example:
    • Name: Cointelegraph (CTDG)
    • Hardware
    • Data Center Name
    • Data Center Location
    • Website
    • Telegram
  6. Sign and submit the update.
  7. Open the candidate page in an incognito browser session.
  8. Verify that the new name is publicly visible.
  9. Wait for the next candidate refresh, or restart the backend.
  10. 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:

MasterNode-App/crawl.js

Lines 269 to 307 in fc925b1

// 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({})

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions