-
Notifications
You must be signed in to change notification settings - Fork 10
/
restore_partial.js
309 lines (283 loc) · 10.7 KB
/
restore_partial.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
/* eslint-disable no-console */
/* eslint-disable no-await-in-loop */
/* eslint-disable no-underscore-dangle */
/**
* Script for helping to restore / repair hive-engine node databases. The restore mode will simply drop the
* existing database and do a full restore.
* The repair mode will delete all invalid blocks / transactions and drop all other collections and restore the
* last valid state using a light node snapshot.
*
* These are the different modes:
* - FULL node:
* - Restore by executing `node restore_partial.js` (~30-60 minutes)
* - Drop by executing `node restore_partial.js -d -s https://snap.primersion.com/` (~6 hours)
* - LIGHT node:
* - Drop by executing `node restore_partial.js -d` (~30-60 minutes)
* - Restore is not supported as dropping is faster
* */
require('dotenv').config();
const program = require('commander');
const axios = require('axios');
const fs = require('fs-extra');
const { exec } = require('child_process');
const conf = require('./config');
const { Database } = require('./libs/Database');
program
.option('-n, --node [url]', 'compare with given node', 'https://api.hive-engine.com/rpc/')
.option('-a, --archive [archive]', 'archive to restore')
.option('-s, --snapshot-url [url]', 'base directory of light node snapshots to download', 'https://snap.primersion.com/light/')
.option('-d, --drop', 'drops the database instead of trying to repair')
.parse(process.argv);
const { node, snapshotUrl, drop } = program;
let { archive } = program;
let id = 1;
/**
* Fetches the hive-engine block with the given @blockNumber from a hive-engine node.
* @param blockNumber to fetch from the node
* @param tries number of retries - will cancel after 3 tries
* @returns {Promise<null|*|undefined>} the hive-engine block or null if the block doesn't exist or the request failed
*/
async function getBlock(blockNumber, tries = 1) {
id += 1;
try {
return (await axios({
url: `${node}`,
method: 'POST',
headers: {
'content-type': 'application/json',
},
data: {
jsonrpc: '2.0', id, method: 'blockchain.getBlockInfo', params: { blockNumber },
},
})).data.result;
} catch (error) {
if (tries >= 3) {
console.error(error);
return null;
}
console.log(`Attempt #${tries} failed, retrying...`);
await new Promise(r => setTimeout(() => r(), 500));
return getBlock(blockNumber, tries + 1);
}
}
/**
* Checks if the local hive-engine node has diverged and is in a different state than the reference @node.
* @param chain connection to the chain mongo collection of the local hive-engine node
* @param lightNode whether the node is a light node or not
* @returns {Promise<number>} return -1 if the node is OK. -2 if the node is not caugth up or there was an error fetching a block or returns the diverged block number if one was found.
*/
async function findDivergentBlock(chain, lightNode) {
let block = (await chain.find().sort({ _id: -1 }).limit(1).toArray())[0];
let mainBlock;
let low = 0;
if (lightNode) {
const firstBlock = await chain.findOne({ blockNumber: { $gt: 0 } });
if (firstBlock) {
low = firstBlock.blockNumber;
}
}
let high = block._id;
const headBlock = high;
while (high - low >= 1) {
console.log(`low ${low} high ${high}`);
const check = Math.floor((low + high) / 2);
mainBlock = await getBlock(check);
if (!mainBlock) {
break;
}
block = await chain.findOne({ _id: check });
// Different comparison modes, uncomment desired comparison.
if (mainBlock.hash !== block.hash) {
high = check;
} else {
low = check + 1;
}
}
if (high === headBlock && high - low <= 0) {
return -1;
}
if (high !== low) {
return -2;
}
return high;
}
/**
* Fetches a list of snapshots from the given snapshotUrl, which should point to a server with directory listing enabled.
* @param tries number of retries - will cancel after 3 tries
* @returns {Promise<any|null|undefined>}
*/
async function fetchSnapshots(tries = 1) {
try {
return (await axios({
url: snapshotUrl,
method: 'GET',
})).data;
} catch (error) {
if (tries >= 3) {
console.error(error);
return null;
}
console.log(`Attempt #${tries} failed, retrying...`);
await new Promise(r => setTimeout(() => r(), 500));
return fetchSnapshots(tries + 1);
}
}
/**
* Download the snapshot with the given @name from the given @url.
* @param url to fetch the snapshot from
* @param name of the snapshot
* @returns {Promise<unknown>} returns a promise which will resolve once the download finished.
*/
async function downloadSnapshot(url, name) {
return axios({
method: 'GET',
url,
responseType: 'stream',
}).then(response => new Promise((resolve, reject) => {
const writer = fs.createWriteStream(`./${name}`);
response.data.pipe(writer);
let error = null;
writer.on('error', (err) => {
error = err;
writer.close();
reject(err);
});
writer.on('close', () => {
if (!error) {
resolve(true);
}
});
}));
}
/**
* Downloads the latest snapshot from the @snapshotUrl, by first fetching all available snapshots and then download the
* latest one to the current directory.
* @returns {Promise<null|*>} blocks until the download finished and returns the downloaded snapshot name afterwards.
*/
async function downloadLatestSnapshot() {
const snapshots = await fetchSnapshots();
if (!snapshots || snapshots.length === 0) {
console.log(`could not find any snapshots at ${snapshotUrl}`);
return null;
}
console.log(`found ${snapshots.length} snapshots at ${snapshotUrl}`);
const snapshot = snapshots[snapshots.length - 1];
console.log(`downloading snapshot ${snapshot.name}`);
await downloadSnapshot(`${snapshotUrl}/${snapshot.name}`, snapshot.name);
console.log(`finished downloading ${snapshot.name}`);
return snapshot.name;
}
/**
* Updates the @startHiveBlock in the config.json file to the @newStartBlock.
* @param newStartBlock to set in the config.json
*/
async function updateConfigJson(newStartBlock) {
const config = fs.readJSONSync('./config.json');
config.startHiveBlock = newStartBlock;
fs.writeJSONSync('./config.json', config, { spaces: 4 });
console.log(`set config.json startHiveBlock to ${newStartBlock}`);
}
/**
* Executes the mongorestore command with the given archive. The restore will be executed in --quiet mode without
* any log output if an existing database is restored as there will be a lot of duplicate key errors otherwise
* for the already existing chain and transaction entries.
* @param archiveName to use for restoring.
*/
async function execMongorestore(archiveName) {
console.log(`starting restore using 'mongorestore --quiet --gzip --archive=${archiveName}'`);
console.log('this will take 30 to 60 minutes without any log output...');
exec(`mongorestore${drop ? '' : ' --quiet'} --gzip --archive=${archiveName}`, (error, stdout, stderr) => {
if (error) {
console.log('failed to restore');
console.log(`error: ${error.message}`);
return;
}
if (stderr) {
console.log('failed to restore');
console.log(`stderr: ${stderr}`);
return;
}
console.log(`stdout: ${stdout}`);
console.log('finished restoring db. now restart your node');
});
}
/**
* Reverts the mongo database by deleting blocks and transactions after a given block and dropping all other collections.
* @param database connection to mongo database
* @param chain connection to the chain collection of the mongo database
* @param divergentBlockNum the divergent block number
* @param archiveHiveBlock the block number of the archive
*/
async function revertDatabase(database, chain, divergentBlockNum, archiveHiveBlock) {
const divergentBlock = await chain.findOne({ _id: divergentBlockNum });
const refHiveBlockDiff = divergentBlock.refHiveBlockNumber - archiveHiveBlock;
const archiveBlockNum = divergentBlockNum - refHiveBlockDiff;
const deleteFromBlock = Math.min(divergentBlockNum, archiveBlockNum) - 7; // delete 7 more blocks just to be on the safe side
console.log(`Divergent block: ${divergentBlockNum} (${divergentBlock.refHiveBlockNumber}) Archive block: ${archiveBlockNum} (${archiveHiveBlock}) Restoring to: ${deleteFromBlock}`);
const collectionsToRemove = await database.database.listCollections().toArray();
for (const col of collectionsToRemove) {
if (col.name === 'system.profile') {
// skip
} else if (col.name !== 'chain' && col.name !== 'transactions') {
console.log(`removing collection ${col.name}`);
await database.database.collection(col.name).drop();
}
}
console.log(`removing blocks >= ${deleteFromBlock}`);
await chain.deleteMany({ _id: { $gte: deleteFromBlock } });
console.log(`removing transactions >= ${deleteFromBlock}`);
await database.database.collection('transactions').deleteMany({ blockNumber: { $gte: deleteFromBlock } });
}
/**
* Executes the restoring process by either dropping an existing databsae and doing a full restore or by checking
* for a divergent state and trying to restore the database by removing invalid blocks / transactions and restoring
* from a light node snapshot.
*/
async function restorePartial() {
const {
databaseURL,
databaseName,
lightNode,
} = conf;
if (lightNode.enabled && !drop) {
console.log('Restoring a light node database is not supported. Please add the \'-d\' flag to completely restore your db.');
return;
}
const database = new Database();
await database.init(databaseURL, databaseName);
const chain = database.database.collection('chain');
let divergentBlockNum = Number.MAX_SAFE_INTEGER;
if (!drop) {
divergentBlockNum = await findDivergentBlock(chain, lightNode.enabled);
if (divergentBlockNum === -1) {
console.log('ok');
await database.close();
return;
}
if (divergentBlockNum === -2) {
console.log('not caught up or error fetching block');
await database.close();
return;
}
console.log(`divergent block id at ${divergentBlockNum}`);
}
if (!archive || typeof archive !== 'string') {
archive = await downloadLatestSnapshot();
if (!archive) {
console.log('start program with \'node restore_partial.js --archive <archive name>\' or add a valid snapshot-url');
return;
}
}
console.log(`restoring from archive ${archive}`);
const archiveHiveBlock = +archive.match(/[0-9]+(?!.*[0-9])/)[0];
if (drop) {
console.log('dropping database');
await database.database.dropDatabase();
} else {
await revertDatabase(database, chain, divergentBlockNum, archiveHiveBlock);
}
await database.close();
await updateConfigJson(archiveHiveBlock);
await execMongorestore(archive);
}
restorePartial();