Skip to content

Commit 3a001e4

Browse files
authored
fix(core-manager): delete the head record when purging a core (#1307)
* test(core-manager): failing test for purged cores reopening in repair mode purgeCore deletes all tree nodes and blocks but leaves the core's head record (fork, length, root hash) in storage. When the same storage is re-opened - as happens when a device re-joins a project after leaving, which re-adds the same core keys - hypercore finds a header claiming length > 0 with no merkle roots and puts the core in repair mode: the replicator becomes push-only and never sends a Synchronize, so the core silently never syncs for the rest of the session. Peers waiting on that core's handshake wait forever. The new test purges a downloaded core, re-opens the same storage, and asserts a block can be re-downloaded. It times out on the current implementation. * fix(core-manager): delete the head record when purging a core purgeCore deletes all tree nodes and blocks but left the core's head record (fork, length, root hash) in storage. When the same storage was re-opened - a device re-joining a project after leaving re-adds the same core keys - hypercore found a header claiming length > 0 with no merkle roots and put the core in repair mode: push-only, never sending a Synchronize, so the core silently never synced for the rest of the session, and peers gating on its handshake waited forever. Delete the head record too, so a purged core re-opens as a normal empty core and re-syncs from peers.
1 parent 7a361b5 commit 3a001e4

2 files changed

Lines changed: 51 additions & 0 deletions

File tree

src/core-manager/index.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,11 @@ async function purgeCore(core) {
522522
const tx = privateCore.storage.write()
523523
tx.deleteTreeNodeRange(0, -1)
524524
tx.deleteBlockRange(0, -1)
525+
// Also delete the head record (fork, length, root hash): a header claiming
526+
// length > 0 with no merkle roots in storage puts the core in hypercore's
527+
// repair mode on next open, in which it never sends a Synchronize and so
528+
// never syncs again
529+
tx.deleteHead()
525530
// @ts-ignore Private methods on storage
526531
privateCore.bitfield.clear(tx)
527532
await tx.flush()

test/core-manager.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import { Transform } from 'streamx'
1818
import { waitForCores } from './helpers/core-manager.js'
1919
import { drizzle } from 'drizzle-orm/better-sqlite3'
2020
import { createCore } from './helpers/create-core.js'
21+
import { temporaryDirectory } from 'tempy'
22+
import fsPromises from 'node:fs/promises'
2123
/** @import { Namespace } from '../src/types.js' */
2224

2325
test('project creator auth core has project key', async function (t) {
@@ -555,3 +557,47 @@ function latencyStream(delay = 0) {
555557
},
556558
})
557559
}
560+
561+
test('deleteOthersData leaves purged cores usable after re-opening storage', async function (t) {
562+
const projectKey = randomBytes(32)
563+
const cm1 = createCoreManager(t, { projectKey })
564+
565+
// cm2 gets stable identity, storage and db so it can be re-opened, as
566+
// happens when a device re-joins a project after leaving
567+
const rootKey = randomBytes(16)
568+
const db = drizzle(new Sqlite(':memory:'))
569+
const storage = temporaryDirectory()
570+
t.after(() => fsPromises.rm(storage, { recursive: true, force: true }))
571+
572+
const cm2 = createCoreManager(t, { projectKey, rootKey, db, storage }, false)
573+
574+
const writer = cm1.getWriterCore('data')
575+
await writer.core.append(['a', 'b', 'c'])
576+
577+
cm2.addCore(writer.key, 'data')
578+
const rep1 = await replicate(cm1, cm2)
579+
const core2 = cm2.getCoreByKey(writer.key)
580+
assert(core2, 'core was added')
581+
await core2.download({ start: 0, end: 3 }).done()
582+
await rep1.destroy()
583+
584+
await cm2.deleteOthersData('data')
585+
await cm2.close()
586+
587+
const cm2reopened = createCoreManager(t, { projectKey, rootKey, db, storage })
588+
cm2reopened.addCore(writer.key, 'data')
589+
const core2reopened = cm2reopened.getCoreByKey(writer.key)
590+
assert(core2reopened, 'core was re-added')
591+
await core2reopened.ready()
592+
593+
const rep2 = await replicate(cm1, cm2reopened)
594+
t.after(() => rep2.destroy())
595+
596+
const block = await Promise.race([
597+
core2reopened.get(0),
598+
delay(5000).then(() => {
599+
throw new Error('timed out re-syncing the purged core')
600+
}),
601+
])
602+
assert.deepEqual(block, Buffer.from('a'), 're-downloaded purged block')
603+
})

0 commit comments

Comments
 (0)