noter: report a scan group in one batch, count progress as liveness (#34)

The first scan after the Zagros People upgrade re-confirmed all 98
accounts, one transaction per account, a block each. It ran past the
15-minute staleness window and the watchdog killed it halfway; the next
interval had meanwhile started a second scan beside it, which read the
same accounts before the first one's reports landed and sent them again
(124 submissions for 98 accounts). On mainnet every tracked citizen's
re-confirmation falls due in the same scan, every three days.

- A scan group's updates go out as one utility.forceBatch. The accounts
  are unrelated, so one refused report no longer takes the others with
  it; each refused item is logged with its decoded error.
- The heartbeat records progress -- a group read and, if it had anything
  to report, reported -- instead of a finished scan. A group that cannot
  be read, or whose report is refused outright, is not progress, so a
  mute noter still goes stale.
- A periodic scan does not start while the previous one is running.
This commit is contained in:
SatoshiQaziMuhammed authored and GitHub committed 2026-10-20 10:25:57 +04:00
1 parent 0220302a98
commit cb25e08547
4 files changed
+179 -25

No files matched your search

+45 -1
View File
@@ -1,7 +1,7 @@
// node --test noter/test
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { watchSubmission, isStale } from '../submit.js';
import { watchSubmission, isStale, failedBatchItems } from '../submit.js';
// A stand-in for an api-derive submittable: it hands the status callback to the test and
// records whether the subscription was released.
@@ -83,3 +83,47 @@ test('a recent scan keeps the bot alive', () => {
assert.equal(isStale({ lastScanAt: 5_000, startedAt: 0, now: 5_500, staleAfter: 900 }), false);
assert.equal(isStale({ lastScanAt: 5_000, startedAt: 0, now: 5_901, staleAfter: 900 }), true);
});
test("a block's events reach onInBlock before the submission resolves", async () => {
const tx = fakeTx();
let seen = null;
const p = watchSubmission(tx, {}, { ...opts, onInBlock: (events) => { seen = events; } });
await tick();
tx.emit({ isInBlock: true, type: 'InBlock', asInBlock: { toHex: () => '0xabc' } }, { events: ['e'] });
assert.equal(await p, '0xabc');
assert.deepEqual(seen, ['e']);
});
test('a dispatch error does not reach onInBlock', async () => {
const tx = fakeTx();
let called = false;
const p = watchSubmission(tx, {}, { ...opts, onInBlock: () => { called = true; } });
await tick();
tx.emit({ isInBlock: true, type: 'InBlock', asInBlock: { toHex: () => '0xabc' } }, { dispatchError: 'E' });
await assert.rejects(p, /decoded E/);
assert.equal(called, false);
});
const ev = (section, method, data = []) => ({ event: { section, method, data } });
test('a failed batch item is reported by its position among the calls', () => {
const events = [
ev('stakingScore', 'StakingDetailsReceived'),
ev('utility', 'ItemCompleted'),
ev('utility', 'ItemFailed', ['NoterCannotAttestSelf']),
ev('stakingScore', 'StakingDetailsReceived'),
ev('utility', 'ItemCompleted'),
ev('utility', 'ItemFailed', ['NotAuthorized']),
ev('utility', 'BatchCompletedWithErrors'),
ev('system', 'ExtrinsicSuccess'),
];
assert.deepEqual(failedBatchItems(events), [
{ index: 1, error: 'NoterCannotAttestSelf' },
{ index: 3, error: 'NotAuthorized' },
]);
});
test('a batch whose every item landed reports no failure', () => {
const events = [ev('utility', 'ItemCompleted'), ev('utility', 'ItemCompleted'), ev('utility', 'BatchCompleted')];
assert.deepEqual(failedBatchItems(events), []);
});