Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## unreleased


### Node changes

- `FullNode` now parses option `--min-weight=<number>` (`min-weight: <number>` in
Expand All @@ -11,6 +12,17 @@ the miner will add transactions up to the minimum weight that would normally be
ignored for being "free" (paying a fee below policy limit). The default value is
raised from `0` to `5000` (a 1-in, 2-out BID transaction has a weight of about `889`).

### Wallet API changes

- Adds new wallet HTTP endpoint `/deepclean` that requires a parameter
`I_HAVE_BACKED_UP_MY_WALLET=true`. This action wipes out balance and transaction
history in the wallet DB but retains key hashes and name maps. It should be used
only if the wallet state has been corrupted by issues like the
[reserved name registration bug](https://github.com/handshake-org/hsd/issues/454)
or the
[locked coins balance after FINALIZE bug](https://github.com/handshake-org/hsd/pull/464).
After the corrupt data has been cleared, **a walletDB rescan is required**.

### Wallet changes

- Fixes a bug that ignored the effect of sending or receiving a FINALIZE on a
Expand Down
20 changes: 20 additions & 0 deletions lib/wallet/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,26 @@ class HTTP extends Server {
await this.wdb.rescan(height);
});

// Deep Clean
this.post('/deepclean', async (req, res) => {
if (!req.admin) {
res.json(403);
return;
}

const valid = Validator.fromRequest(req);
const disclaimer = valid.bool('I_HAVE_BACKED_UP_MY_WALLET', false);

enforce(
disclaimer,
'Deep Clean requires I_HAVE_BACKED_UP_MY_WALLET=true'
);

res.json(200, { success: true });

await this.wdb.deepClean();
});

// Resend
this.post('/resend', async (req, res) => {
if (!req.admin) {
Expand Down
87 changes: 87 additions & 0 deletions lib/wallet/walletdb.js
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,93 @@ class WalletDB extends EventEmitter {
}
}

/**
* Deep Clean:
* Keep all keys, account data, wallet maps (name and path).
* Dump all TX history and balance state.
* A rescan will be required but is not initiated automatically.
* @returns {Promise}
*/

async deepClean() {
const unlock1 = await this.txLock.lock();
const unlock2 = await this.writeLock.lock();
const unlock3 = await this.readLock.lock();
try {
return await this._deepClean();
} finally {
unlock3();
unlock2();
unlock1();
}
}

/**
* Deep Clean (without locks):
* Keep all keys, account data, wallet maps (name and path).
* Dump all TX history and balance state.
* A rescan will be required but is not initiated automatically.
* @returns {Promise}
*/

async _deepClean() {
this.logger.warning('Initiating Deep Clean...');

const b = this.db.batch();
const removeRange = (opt) => {
return this.db.iterator(opt).each(key => b.del(key));
};

this.logger.warning('Clearing block map, tx map and outpoint map...');
// b[height] -> block->wid map
await removeRange({
gte: layout.b.min(),
lte: layout.b.max()
});

// o[hash][index] -> outpoint->wid map
await removeRange({
gte: layout.o.min(),
lte: layout.o.max()
});

// T[hash] -> tx->wid map
await removeRange({
gte: layout.T.min(),
lte: layout.T.max()
});

const wids = await this.db.keys({
gte: layout.W.min(),
lte: layout.W.max(),
parse: key => layout.W.decode(key)[0]
});

for (const wid of wids) {
const wallet = await this.get(wid);
this.logger.warning(
'Clearing all tx history for wallet: %s (%d)',
wallet.id, wid
);

// remove all txdb data *except* blinds ('v')
const key = 'v'.charCodeAt();
const prefix = layout.t.encode(wid);
await removeRange({
gte: Buffer.concat([prefix, Buffer.alloc(1)]),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this supposed to be a 1 or a 0?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It "should" be a single 0x00 byte buffer. This code will remove everything in the txdb up to the blinds. The next snippet removes everything in the txdb after the blinds:

Removed: 0x00 -> v and v -> 0xff

lt: Buffer.concat([prefix, Buffer.from([key])])
});
await removeRange({
gt: Buffer.concat([prefix, Buffer.from([key + 1])]),
lte: Buffer.concat([prefix, Buffer.from([0xff])])
});
}

await b.write();

this.logger.warning('Deep Clean complete. A rescan is now required.');
}

/**
* Force a rescan.
* @param {Number} height
Expand Down
Loading