proof.ts 10.9 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
// Source file from duniter: Crypto-currency software to manage libre currency such as Ğ1
// Copyright (C) 2018  Cedric Moreau <cem.moreau@gmail.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.

14
15
16
17
import {LOCAL_RULES_HELPERS} from "../../../lib/rules/local_rules"
import {hashf} from "../../../lib/common"
import {DBBlock} from "../../../lib/db/DBBlock"
import {ConfDTO} from "../../../lib/dto/ConfDTO"
18
import {ProverConstants} from "./constants"
19
import {KeyGen} from "../../../lib/common-libs/crypto/keyring"
20
import {dos2unix} from "../../../lib/common-libs/dos2unix"
21
import {rawer} from "../../../lib/common-libs/index"
22
import {ProcessCpuProfiler} from "../../../ProcessCpuProfiler"
23
import {PowDAL} from "../../../lib/dal/fileDALs/PowDAL";
24
25
26

const moment = require('moment');
const querablep = require('querablep');
27
const directory = require('../../../lib/system/directory');
28

29
export function createPowWorker() {
30

31
  let powDAL:PowDAL|null = null
32
33
  let computing = querablep(Promise.resolve(null));
  let askedStop = false;
34

35
36
// By default, we do not prefix the PoW by any number
  let prefix = 0;
37

38
  let signatureFunc:any, lastSecret:any, currentCPU = 1;
39

40
41
42
43
44
45
46
47
  process.on('uncaughtException', (err:any) => {
    console.error(err.stack || Error(err))
    if (process.send) {
      process.send({error: err});
    } else {
      throw Error('process.send() is not defined')
    }
  });
48

49
50
51
  process.on('unhandledRejection', () => {
    process.exit()
  })
52

53
  process.on('message', async (message) => {
54

55
    switch (message.command) {
56

57
58
59
      case 'newPoW':
        (async () => {
          askedStop = true
60

61
62
63
64
          // Very important: do not await if the computation is already done, to keep the lock on JS engine
          if (!computing.isFulfilled()) {
            await computing;
          }
65

66
67
68
69
70
          if (message.value.rootPath) {
            const params = await directory.getHomeFS(false, message.value.rootPath, false)
            powDAL = new PowDAL(message.value.rootPath, params.fs)
          }

71
72
73
74
          const res = await beginNewProofOfWork(message.value);
          answer(message, res);
        })()
        break;
75

76
77
78
79
80
      case 'cancel':
        if (!computing.isFulfilled()) {
          askedStop = true;
        }
        break;
81

82
83
84
85
86
87
88
89
90
      case 'conf':
        if (message.value.cpu !== undefined) {
          currentCPU = message.value.cpu
        }
        if (message.value.prefix !== undefined) {
          prefix = message.value.prefix
        }
        answer(message, { currentCPU, prefix });
        break;
91
92
    }

93
94
95
96
97
  })

  function beginNewProofOfWork(stuff:any) {
    askedStop = false;
    computing = querablep((async () => {
98

99
100
101
      /*****************
       * PREPARE POW STUFF
       ****************/
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
      let nonce = 0;
      const maxDuration = stuff.maxDuration || 1000
      const conf = stuff.conf;
      const block = stuff.block;
      const nonceBeginning = stuff.nonceBeginning;
      const nbZeros = stuff.zeros;
      const pair = stuff.pair;
      const forcedTime = stuff.forcedTime;
      currentCPU = conf.cpu || ProverConstants.DEFAULT_CPU;
      prefix = parseInt(conf.prefix || prefix)
      if (prefix && prefix < ProverConstants.NONCE_RANGE) {
        prefix *= 100 * ProverConstants.NONCE_RANGE
      }
      const highMark = stuff.highMark;
      let sigFunc = null;
      if (signatureFunc && lastSecret === pair.sec) {
        sigFunc = signatureFunc;
      }
      else {
        lastSecret = pair.sec;
        sigFunc = (msg:string) => KeyGen(pair.pub, pair.sec).signSync(msg)
      }
      signatureFunc = sigFunc;
      let pow = "", sig = "", raw = "";
127
128

      /*****************
129
       * GO!
130
131
       ****************/

132
133
134
135
136
137
138
139
140
      let pausePeriod = 1;
      let testsCount = 0;
      let found = false;
      let turn = 0;
      const profiler = new ProcessCpuProfiler(100)
      let cpuUsage = profiler.cpuUsageOverLastMilliseconds(1)
      // We limit the number of tests according to CPU usage
      let testsPerRound = stuff.initialTestsPerRound || 1
      let turnDuration = 20 // We initially goes quickly to the max speed = 50 reevaluations per second (1000 / 20)
141

142
      while (!found && !askedStop) {
143

144
145
146
        /*****************
         * A TURN ~ 100ms
         ****************/
147

148
        await Promise.race([
149

150
151
          // I. Stop the turn if it exceeds `turnDuration` ms
          countDown(turnDuration),
152

153
154
          // II. Process the turn's PoW
          (async () => {
155

156
157
158
            // Prove
            let i = 0;
            const thisTurn = turn;
159

160
161
162
163
164
165
            // Time is updated regularly during the proof
            block.time = getBlockTime(block, conf, forcedTime)
            if (block.number === 0) {
              block.medianTime = block.time
            }
            block.inner_hash = getBlockInnerHash(block);
166
167

            /*****************
168
             * Iterations of a turn
169
170
             ****************/

171
            while(!found && i < testsPerRound && thisTurn === turn && !askedStop) {
172

173
174
              // Nonce change (what makes the PoW change if the time field remains the same)
              nonce++
175

176
177
178
              /*****************
               * A PROOF OF WORK
               ****************/
179

180
181
182
183
184
              // The final nonce is composed of 3 parts
              block.nonce = prefix + nonceBeginning + nonce
              raw = dos2unix("InnerHash: " + block.inner_hash + "\nNonce: " + block.nonce + "\n")
              sig = dos2unix(sigFunc(raw))
              pow = hashf("InnerHash: " + block.inner_hash + "\nNonce: " + block.nonce + "\n" + sig + "\n").toUpperCase()
185

186
187
188
189
190
191
192
193
194
195
196
197
198
199
              /*****************
               * Check the POW result
               ****************/

              let j = 0, charOK = true;
              while (j < nbZeros && charOK) {
                charOK = pow[j] === '0';
                j++;
              }
              if (charOK) {
                found = !!(pow[nbZeros].match(new RegExp('[0-' + highMark + ']')))
              }
              if (!found && nbZeros > 0 && j - 1 >= ProverConstants.POW_MINIMAL_TO_SHOW) {
                pSend({ pow: { pow: pow, block: block, nbZeros: nbZeros }});
200
201
              }

202
203
204
205
206
207
208
209
210
              /*****************
               * - Update local vars
               * - Allow to receive stop signal
               ****************/

              if (!found && !askedStop) {
                i++;
                testsCount++;
                if (i % pausePeriod === 0) {
211
                  await countDown(1); // Very low pause, just the time to process eventual end of the turn
212
213
                }
              }
214
215
216
            }

            /*****************
217
             * Check the POW result
218
             ****************/
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
            if (!found) {

              // CPU speed recording
              if (turn > 0) {
                cpuUsage = profiler.cpuUsageOverLastMilliseconds(turnDuration)
                if (cpuUsage > currentCPU + 0.005 || cpuUsage < currentCPU - 0.005) {
                  let powVariationFactor
                  // powVariationFactor = currentCPU / (cpuUsage || 0.01) / 5 // divide by 2 to avoid extreme responses
                  if (currentCPU > cpuUsage) {
                    powVariationFactor = 1.01
                    testsPerRound = Math.max(1, Math.ceil(testsPerRound * powVariationFactor))
                  } else {
                    powVariationFactor = 0.99
                    testsPerRound = Math.max(1, Math.floor(testsPerRound * powVariationFactor))
                  }
                  pausePeriod = Math.floor(testsPerRound / ProverConstants.POW_NB_PAUSES_PER_ROUND)
                }
              }
237

238
239
240
241
242
243
244
245
246
247
248
              /*****************
               * UNLOAD CPU CHARGE FOR THIS TURN
               ****************/
              // We wait for a maximum time of `turnDuration`.
              // This will trigger the end of the turn by the concurrent race I. During that time, the proof.js script
              // just does nothing: this gives of a bit of breath to the CPU. Tthe amount of "breath" depends on the "cpu"
              // parameter.
              await countDown(turnDuration);
            }
          })()
        ]);
249

250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
        // console.log('W#%s.powDAL = ', process.pid, powDAL)

        if (powDAL && !conf.powNoSecurity) {
          const currentProofCheck = await powDAL.getCurrent()
          if (currentProofCheck !== null) {
            if (currentProofCheck === "") {
              askedStop = true
            } else {
              const [currentNumber, currentHash] = currentProofCheck.split('-')
              if (block.number !== parseInt(currentNumber) + 1 || block.previousHash !== currentHash) {
                askedStop = true
              }
            }
          }
        }

266
267
        // Next turn
        turn++
268

269
270
271
        turnDuration += 1
        turnDuration = Math.min(turnDuration, maxDuration) // Max 1 second per turn
      }
272

273
274
275
276
277
278
      /*****************
       * POW IS OVER
       * -----------
       *
       * We either have found a valid POW or a stop event has been detected.
       ****************/
279

280
      if (askedStop) {
281

282
283
284
285
        // PoW stopped
        askedStop = false;
        pSend({ canceled: true })
        return null
286

287
288
289
290
291
292
293
294
295
296
297
      } else {

        // PoW success
        block.hash = pow
        block.signature = sig
        return {
          pow: {
            block: block,
            testsCount: testsCount,
            pow: pow
          }
298
299
        }
      }
300
    })())
301

302
303
    return computing;
  }
304

305
306
307
  function countDown(duration:number) {
    return new Promise((resolve) => setTimeout(resolve, duration));
  }
308

309
310
311
312
  function getBlockInnerHash(block:DBBlock) {
    const raw = rawer.getBlockInnerPart(block);
    return hashf(raw)
  }
313

314
315
316
317
318
319
320
321
322
323
  function getBlockTime (block:DBBlock, conf:ConfDTO, forcedTime:number|null) {
    if (forcedTime) {
      return forcedTime;
    }
    const now = moment.utc().unix();
    const maxAcceleration = LOCAL_RULES_HELPERS.maxAcceleration(conf);
    const timeoffset = block.number >= conf.medianTimeBlocks ? 0 : conf.rootoffset || 0;
    const medianTime = block.medianTime;
    const upperBound = block.number === 0 ? medianTime : Math.min(medianTime + maxAcceleration, now - timeoffset);
    return Math.max(medianTime, upperBound);
324
325
  }

326
327
328
329
330
331
  function answer(message:any, theAnswer:any) {
    return pSend({
      uuid: message.uuid,
      answer: theAnswer
    })
  }
332

333
334
335
336
337
338
339
340
341
342
343
344
  function pSend(stuff:any) {
    return new Promise(function (resolve, reject) {
      if (process.send) {
        process.send(stuff, function (error:any) {
          !error && resolve();
          error && reject();
        })
      } else {
        reject('process.send() is not defined')
      }
    });
  }
345
}