proof.ts 11.2 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
import * as moment from "moment"
15 16 17 18
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"
19
import {ProverConstants} from "./constants"
20
import {KeyGen} from "../../../lib/common-libs/crypto/keyring"
21
import {dos2unix} from "../../../lib/common-libs/dos2unix"
22
import {rawer} from "../../../lib/common-libs/index"
23
import {ProcessCpuProfiler} from "../../../ProcessCpuProfiler"
24
import {PowDAL} from "../../../lib/dal/fileDALs/PowDAL";
25
import {Directory} from "../../../lib/system/directory"
26
import {ExitCodes} from "../../../lib/common-libs/exit-codes"
27 28 29

const querablep = require('querablep');

30
export function createPowWorker() {
31

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

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

39
  let signatureFunc:any, lastSecret:any, lastVersion: number, currentCPU = 1;
40

41 42 43 44 45 46 47 48
  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')
    }
  });
49

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

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

56
    switch (message.command) {
57

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

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

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

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

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

83 84 85 86 87 88 89 90 91
      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;
92 93
    }

94 95 96 97 98
  })

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

100 101 102
      /*****************
       * PREPARE POW STUFF
       ****************/
103

104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
      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;
119
      if (signatureFunc && lastSecret === pair.sec && lastVersion === block.version) {
120
        sigFunc = signatureFunc;
121
      } else {
122
        lastSecret = pair.sec;
123 124 125 126 127 128
        lastVersion = block.version;
        if (block.version >= 12) {
          sigFunc = (msg:string) => KeyGen(pair.pub, pair.sec).signSync(msg)
        } else {
          sigFunc = (msg:string) => KeyGen(pair.pub, pair.sec).signSyncBuggy(msg)
        }
129 130
      }
      let pow = "", sig = "", raw = "";
131 132

      /*****************
133
       * GO!
134 135
       ****************/

136 137 138 139 140 141 142 143 144
      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)
145

146
      while (!found && !askedStop) {
147

148 149 150
        /*****************
         * A TURN ~ 100ms
         ****************/
151

152
        await Promise.race([
153

154 155
          // I. Stop the turn if it exceeds `turnDuration` ms
          countDown(turnDuration),
156

157 158
          // II. Process the turn's PoW
          (async () => {
159

160 161 162
            // Prove
            let i = 0;
            const thisTurn = turn;
163

164 165 166 167 168 169
            // 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);
170 171

            /*****************
172
             * Iterations of a turn
173 174
             ****************/

175
            while(!found && i < testsPerRound && thisTurn === turn && !askedStop) {
176

177 178
              // Nonce change (what makes the PoW change if the time field remains the same)
              nonce++
179

180 181 182
              /*****************
               * A PROOF OF WORK
               ****************/
183

184 185 186 187 188
              // 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()
189

190 191 192 193 194 195 196 197 198 199 200 201 202 203
              /*****************
               * 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 }});
204 205
              }

206 207 208 209 210 211 212 213 214
              /*****************
               * - Update local vars
               * - Allow to receive stop signal
               ****************/

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

            /*****************
221
             * Check the POW result
222
             ****************/
223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240
            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)
                }
              }
241

242 243 244 245 246 247 248 249 250 251 252
              /*****************
               * 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);
            }
          })()
        ]);
253

254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269
        // 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
              }
            }
          }
        }

270 271
        // Next turn
        turn++
272

273 274 275
        turnDuration += 1
        turnDuration = Math.min(turnDuration, maxDuration) // Max 1 second per turn
      }
276

277 278 279 280 281 282
      /*****************
       * POW IS OVER
       * -----------
       *
       * We either have found a valid POW or a stop event has been detected.
       ****************/
283

284
      if (askedStop) {
285

286 287 288 289
        // PoW stopped
        askedStop = false;
        pSend({ canceled: true })
        return null
290

291 292 293 294 295 296 297 298 299 300 301
      } else {

        // PoW success
        block.hash = pow
        block.signature = sig
        return {
          pow: {
            block: block,
            testsCount: testsCount,
            pow: pow
          }
302 303
        }
      }
304
    })())
305

306 307
    return computing;
  }
308

309 310 311
  function countDown(duration:number) {
    return new Promise((resolve) => setTimeout(resolve, duration));
  }
312

313 314 315 316
  function getBlockInnerHash(block:DBBlock) {
    const raw = rawer.getBlockInnerPart(block);
    return hashf(raw)
  }
317

318 319 320 321 322 323 324 325 326 327
  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);
328 329
  }

330 331 332 333 334 335
  function answer(message:any, theAnswer:any) {
    return pSend({
      uuid: message.uuid,
      answer: theAnswer
    })
  }
336

337 338 339 340 341 342 343 344 345 346 347 348
  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')
      }
    });
  }
349
}