Repository navigation
Masternode identity metadata is erased by the candidate refresh every 150 blocks #66
Description
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
- Application: https://master.apothem.network/
- Network: Apothem testnet
- Candidate:
xdce7a19c8113414b2643a39a6be16843d38f6be923 - Owner:
xdc9ae43d7047bd683322f778402ffad9a760c5ddd7 - Candidate page:
https://master.apothem.network/candidate/xdce7a19c8113414b2643a39a6be16843d38f6be923 - Observed on: July 21, 2026
- Repository revision:
fc925b16578d28aa2283ee3144926c88fb26ae7e
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
- Name:
- 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:
MasterNode-App/apis/candidates.js
Lines 793 to 860 in fc925b1
| 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:
namehardwaredataCenter.namedataCenter.locationsocials.websitesocials.telegram
However, getCurrentCandidates() reads the previous candidate documents and
then deletes the entire candidate collection:
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({})
Activity
loglapa commentedon Jul 24, 2026
Confirmed on XDC Mainnet
The same issue is reproducible on the XDC Mainnet Governance Dashboard.
Environment:
xdc825c8fd54dfed61dc9fa39ff919c2e25003fdc93xdcefaca25648b059dd2dcc66192a373a845246c659https://master.xinfin.network/candidate/xdc825c8fd54dfed61dc9fa39ff919c2e25003fdc93
The following identity metadata was successfully saved and initially displayed
publicly:
Cointelegraph CTDG112 vCPU / 32 GB / 1 TBOVHcloudFranceAfter the next crawler refresh, all identity fields disappeared.
The current candidate API response still contains the correct blockchain-derived
data:
10,000,000 XDCSTANDBYHowever, it no longer contains:
namehardwaredataCentersocialsCurrent API:
https://master.xinfin.network/api/candidates/xdc825c8fd54dfed61dc9fa39ff919c2e25003fdc93
The candidate document was created at:
2026-07-24T13:47:31.864Zand updated without the identity metadata at:
2026-07-24T13:52:58.265ZThe approximately five-minute interval is consistent with the candidate refresh
that runs every 150 blocks.
This confirms that the issue affects both Apothem and XDC Mainnet.