Skip to content

Commit

Permalink
chain: save tree root to DB before compacting for failure recovery
Browse files Browse the repository at this point in the history
  • Loading branch information
pinheadmz committed Apr 12, 2022
1 parent 63733a5 commit 6cfd96b
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 3 deletions.
8 changes: 5 additions & 3 deletions lib/blockchain/chain.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,11 @@ class Chain extends AsyncEmitter {

const {treeInterval} = this.network.names;

// Current state of the tree, freshly loaded from disk.
// It will be in the most recently-committed state,
// which was at the last tree interval. There might have been
// Current state of the tree, loaded from chain database and
// injected in chainDB.open(). It should be in the most
// recently-committed state, which should have been at the last
// tree interval. We might also need to recover from a
// failed compactTree() operation. Either way, there might have been
// new blocks added to the chain since then.
const currentRoot = this.db.treeRoot();

Expand Down
8 changes: 8 additions & 0 deletions lib/blockchain/chaindb.js
Original file line number Diff line number Diff line change
Expand Up @@ -944,6 +944,14 @@ class ChainDB {
*/

async compactTree(root) {
// Before doing anything to the tree,
// save the target tree root hash to chain database.
// If the tree data gets out of sync or corrupted
// the chain database knows where to resync the tree from.
this.start();
this.put(layout.s.encode(), root);
await this.commit();

// Rewind tree to historical commitment
await this.tree.inject(root);

Expand Down
47 changes: 47 additions & 0 deletions test/chain-tree-compaction-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,53 @@ describe('Tree Compacting', function() {
assert.bufferEqual(ns.data, Buffer.from([counter - 1]));
});

it('should recover if aborted', async () => {
// Get current counter value.
let raw = await chain.db.tree.get(nameHash);
let ns = NameState.decode(raw);
const counter = ns.data[0];

// Add 20 tree intervals
for (let i = counter; i <= counter + 20; i++) {
send(await wallet.sendUpdate(name, Buffer.from([i])), mempool);
await mineBlocks(treeInterval, mempool);
}

const before = await fs.stat(treePath);

// Rewind the tree 6 intervals and compact, but do not sync to tip yet.
const entry = await chain.getEntry(chain.height - 6 * treeInterval);
await chain.db.compactTree(entry.treeRoot);

// Confirm tree state has been rewound
assert.notBufferEqual(chain.db.tree.rootHash(), chain.tip.treeRoot);

// Oops, we abort before calling chain.syncTree()
await miner.close();
await chain.close();
await blocks.close();

// Restart -- chainDB used to open tree with what it thought
// was the latest tree state (saved in levelDB). If the actual
// tree on disk was still 6 intervals behind, chain.open() would
// fail with `Missing node` error. The updated logic relies on the
// tree itself to find its own state (saved in Meta nodes) then
// chain.syncTree() will catch it up from there to tip.
await blocks.open();
await chain.open();
await miner.open();

// Tree was compacted
const after = await fs.stat(treePath);
assert(before.size > after.size);

// Tree was re-synced automatically to chain tip on restart
assert.bufferEqual(chain.db.tree.rootHash(), chain.tip.treeRoot);
raw = await chain.db.tree.get(nameHash);
ns = NameState.decode(raw);
assert.bufferEqual(ns.data, Buffer.from([counter + 20]));
});

it(`should ${prune ? '' : 'not '}have pruned chain`, async () => {
// Sanity check. Everything worked on a chain that is indeed pruning.
// Start at height 2 because pruneAfterHeight == 1
Expand Down

0 comments on commit 6cfd96b

Please sign in to comment.