From 92022d79c119f4df89e712167f711e0e1aaff544 Mon Sep 17 00:00:00 2001 From: Satoshi Qazi Muhammed Date: Tue, 14 Jul 2026 17:51:08 -0700 Subject: [PATCH] Fix noter bot: serialize signAndSend to stop concurrent-nonce tx rejection fullScan() processes accounts in concurrent batches of 10, and each account's own data-gathering takes variable time, so multiple accounts' submitStakingDetails calls landed close enough together to race on nonce assignment - the pool then rejected most of them as duplicate-priority ("Priority is too low ... already in the pool"). Observed live: 37 of 50 tracked accounts silently dropped in a single scan cycle, including real accounts with genuine stake (Serok, QaziM) whose trust scores stayed stuck at 0 as a result even after the separate NotRegisteredNoter bond issue was fixed. Routes every submission (both fullScan's batches and the live event listener) through a single queue so exactly one signAndSend is ever in flight at a time, in call order - data-gathering stays concurrent (unaffected), only the actual chain submission is serialized. --- noter/bot.js | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/noter/bot.js b/noter/bot.js index 72fea3a..ac091d6 100644 --- a/noter/bot.js +++ b/noter/bot.js @@ -232,13 +232,32 @@ function hasDataChanged(fresh, cached) { // TRANSACTION SUBMISSION // ======================================== +/** + * Serializes every real submission through this queue. fullScan() fires up to BATCH_SIZE + * processAccount() calls concurrently (by design, for the read-only data-gathering they start + * with), and the live event listener can also submit at any time - without this, multiple + * signAndSend() calls from the same noter keypair can land close enough together that the API's + * nonce bookkeeping races, producing duplicate-nonce transactions the pool then rejects as + * "Priority is too low ... already in the pool" (observed: 37 of 50 accounts silently dropped in + * a single scan). One in-flight submission at a time, in call order, removes the race entirely + * regardless of which caller is submitting. + */ +let submitQueue = Promise.resolve(); + /** * Submit receive_staking_details for one or more (address, source) pairs. * Batches multiple calls into a single utility.batchAll transaction. */ -async function submitStakingDetails(peopleApi, noterKeypair, updates) { - if (updates.length === 0) return; +function submitStakingDetails(peopleApi, noterKeypair, updates) { + if (updates.length === 0) return Promise.resolve(); + const task = submitQueue.then(() => doSubmitStakingDetails(peopleApi, noterKeypair, updates)); + // Never let one caller's failure poison the queue for the next caller's submission. + submitQueue = task.catch(() => {}); + return task; +} + +function doSubmitStakingDetails(peopleApi, noterKeypair, updates) { const calls = updates.map(({ address, source, data }) => peopleApi.tx.stakingScore.receiveStakingDetails( address,