Skip to content

Commit dffd406

Browse files
web3blindclaude
andcommitted
Fix critical bugs in chain recovery: dedup blocks, race condition, headBlock
1. Prevent duplicate block processing: track processed blocks in _recoveryProcessedBlocks map, shared between user chain and secondary account chain recovery 2. Fix _pollBusy race condition: keep _pollBusy=true during entire recovery so poll ticks don't fire concurrently 3. Fix worldState.headBlock: set to current chain head after recovery so a crash+restart doesn't trigger a huge catch-up from old block 4. Type safety: check typeof before calling _rememberAccount on action.data.target/to to avoid storing non-string values Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 61f577d commit dffd406

File tree

2 files changed

+45
-16
lines changed

2 files changed

+45
-16
lines changed

app/js/engine/state-engine.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -349,8 +349,8 @@ var StateEngine = (function() {
349349
// Add to recent actions — remember sender and any target accounts
350350
_rememberAccount(sender);
351351
if (action.data) {
352-
if (action.data.target) _rememberAccount(action.data.target);
353-
if (action.data.to) _rememberAccount(action.data.to);
352+
if (typeof action.data.target === 'string') _rememberAccount(action.data.target);
353+
if (typeof action.data.to === 'string') _rememberAccount(action.data.to);
354354
}
355355

356356
worldState.recentActions.push({

app/js/ui/app.js

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,20 @@ var App = (function() {
267267
var _chainRecoveryDone = false;
268268
/** Whether chain recovery is currently running */
269269
var _chainRecoveryBusy = false;
270+
/** Set of block numbers already processed during recovery to prevent duplicates */
271+
var _recoveryProcessedBlocks = {};
272+
273+
/**
274+
* Process a single block during recovery, skipping if already seen.
275+
* Prevents duplicate items/XP when the same block appears in
276+
* multiple accounts' chains.
277+
*/
278+
function _processRecoveryBlock(blockNum, block) {
279+
if (_recoveryProcessedBlocks[blockNum]) return;
280+
_recoveryProcessedBlocks[blockNum] = true;
281+
var processed = BlockProcessor.processBlock(block, blockNum);
282+
StateEngine.processBlock(processed);
283+
}
270284

271285
/**
272286
* Recover the current user's full action history by traversing the
@@ -282,6 +296,9 @@ var App = (function() {
282296
* After recovery finishes, normal 24-hour forward polling takes over
283297
* for shared/world state (other players' guild invites, world boss,
284298
* marketplace listings, etc.).
299+
*
300+
* @param {number} headBlock - current chain head from dgp
301+
* @param {Function} callback - called when recovery is complete
285302
*/
286303
function _recoverChainHistory(headBlock, callback) {
287304
if (_chainRecoveryBusy) { callback(); return; }
@@ -313,9 +330,12 @@ var App = (function() {
313330
// Collect block numbers that are OLDER than the 24h window
314331
// (blocks inside the window will be processed by normal polling)
315332
var historicalBlocks = [];
333+
var seen = {};
316334
for (var i = 0; i < actions.length; i++) {
317-
if (actions[i].blockNum < recentWindowStart) {
318-
historicalBlocks.push(actions[i].blockNum);
335+
var bn = actions[i].blockNum;
336+
if (bn < recentWindowStart && !seen[bn]) {
337+
seen[bn] = true;
338+
historicalBlocks.push(bn);
319339
}
320340
}
321341

@@ -337,11 +357,18 @@ var App = (function() {
337357
function processNext() {
338358
if (idx >= historicalBlocks.length) {
339359
// After user's own chain is recovered, discover other players
340-
_recoverKnownAccountsChains(recentWindowStart, function() {
360+
_recoverKnownAccountsChains(recentWindowStart, headBlock, function() {
361+
// Set headBlock to current chain head so checkpoint is up-to-date.
362+
// Without this, worldState.headBlock would point to the last
363+
// historical block and a crash+restart would trigger a huge catch-up.
364+
var state = StateEngine.getState();
365+
state.headBlock = headBlock;
366+
341367
StateEngine.saveCheckpoint(function() {
342368
console.log('App: Chain history recovery complete,', historicalBlocks.length, 'blocks processed');
343369
_chainRecoveryDone = true;
344370
_chainRecoveryBusy = false;
371+
_recoveryProcessedBlocks = {}; // free memory
345372
callback();
346373
});
347374
});
@@ -360,8 +387,7 @@ var App = (function() {
360387
return;
361388
}
362389

363-
var processed = BlockProcessor.processBlock(block, blockNum);
364-
StateEngine.processBlock(processed);
390+
_processRecoveryBlock(blockNum, block);
365391
idx++;
366392

367393
// Small delay to avoid hammering the node
@@ -380,8 +406,9 @@ var App = (function() {
380406
* marketplace listings, and the social feed account list.
381407
* Processes up to 100 actions per account, only blocks older than
382408
* the 24h window (to avoid overlap with normal polling).
409+
* Uses _recoveryProcessedBlocks to skip blocks already seen.
383410
*/
384-
function _recoverKnownAccountsChains(recentWindowStart, callback) {
411+
function _recoverKnownAccountsChains(recentWindowStart, currentHead, callback) {
385412
var state = StateEngine.getState();
386413
var user = VizAccount.getCurrentUser();
387414
var others = [];
@@ -419,11 +446,12 @@ var App = (function() {
419446
return;
420447
}
421448

422-
// Collect historical blocks for this account
449+
// Collect historical blocks for this account, skip already-processed
423450
var blocks = [];
424451
for (var i = 0; i < actions.length; i++) {
425-
if (actions[i].blockNum < recentWindowStart) {
426-
blocks.push(actions[i].blockNum);
452+
var bn = actions[i].blockNum;
453+
if (bn < recentWindowStart && !_recoveryProcessedBlocks[bn]) {
454+
blocks.push(bn);
427455
}
428456
}
429457

@@ -441,10 +469,10 @@ var App = (function() {
441469
return;
442470
}
443471

444-
viz.api.getBlock(blocks[bIdx], function(bErr, block) {
472+
var blockNum = blocks[bIdx];
473+
viz.api.getBlock(blockNum, function(bErr, block) {
445474
if (!bErr && block) {
446-
var processed = BlockProcessor.processBlock(block, blocks[bIdx]);
447-
StateEngine.processBlock(processed);
475+
_processRecoveryBlock(blockNum, block);
448476
}
449477
bIdx++;
450478
setTimeout(nextBlock, 10);
@@ -486,10 +514,11 @@ var App = (function() {
486514
// On a fresh device (no checkpoint), first recover the user's
487515
// full action history via backward chain traversal before
488516
// starting the normal 24h forward replay.
517+
// Keep _pollBusy=true so no other poll ticks fire during recovery.
489518
if (!_chainRecoveryDone) {
490-
_pollBusy = false;
491519
_recoverChainHistory(headBlock, function() {
492-
// Recovery done — next poll tick will set the 24h window
520+
// Recovery done — release lock, next tick sets 24h window
521+
_pollBusy = false;
493522
});
494523
return;
495524
}

0 commit comments

Comments
 (0)