diff --git a/clients/nodejs/validator-registration.js b/clients/nodejs/validator-registration.js new file mode 100644 index 000000000..28582bc61 --- /dev/null +++ b/clients/nodejs/validator-registration.js @@ -0,0 +1,365 @@ +/** + * Stores the validator configuration that will be registered + * It also generates the data field for the txns that are necessary to register a new validator + */ +class ValidatorRegistry { + /** + * @param {Address} validator_address + * @param {string} signing_key + * @param {BLSPublicKey} voting_key + */ + constructor(validator_address, signing_key, voting_key) { + /** @type {Address} */ + this._validator_address = validator_address; + /** @type {string} */ + this._signing_key = signing_key; + /** @type {BLSPublicKey} */ + this._voting_key = voting_key; + } + + /** + * Obtains the serialized data for validator registration + * (All sizes in bytes) + * + * <- 1 -><- 11 -> <- 32 -><- 20 -> + * | Type | Unused | Signing key | Validator Address | + * + * <- 1 -><- 6 -> <- 57 -> + * | Type | Unused | Voting key[0] | + * + * <- 1 -><- 6 -> <- 57 -> + * | Type | Unused | Voting key[1] | + * + * <- 1 -><- 6 -> <- 57 -> + * | Type | Unused | Voting key[2] | + * + * <- 1 -><- 6 -> <- 57 -> + * | Type | Unused | Voting key[3] | + * + * <- 1 -><- 6 -> <- 57 -> + * | Type | Unused | Voting key[4] | + */ + get_serialized_data() { + const txns_data = []; + + // Serialize first part of the transaction data + let data = new Nimiq.SerialBuffer(64); + // First byte: type + let type = 1; + data.writeUint8(type); + + // Unused bytes + const unused = new Uint8Array(11); + data.write(unused); + + // Signing key and validator address + this._signing_key.serialize(data); + this._validator_address.serialize(data); + + // We are done with the first part of the transaction data + txns_data.push(data); + + // Extract the voting key + const voting_key = Nimiq.BufferUtils.fromHex(this._voting_key.toHex()); + + if (voting_key.length != 285) { + console.error('Invalid voting key'); + process.exit(1); + } + + // Create the next transactions + const vk_unused = new Uint8Array(6); + + for (let i = 0; i < 5; i++) { + data = new Nimiq.SerialBuffer(64); + type += 1; + data.writeUint8(type); + data.write(vk_unused); + // We read the voting key in 57 bytes chunks + data.write(voting_key.read(57)); + txns_data.push(data); + } + + return txns_data; + } +} + +function help() { + console.log(`Nimiq NodeJS tool to register a new validator + +Usage: + node validator.js [options] + +Description: + If no arguments are provided all validator keys are generated and stored in a JSONC file + Otherwise, the user must specify the path to the JSONC file with the validator configuration: + The Validator Account, the Signing Key and the Voting Key. + The nimiq core-web package is necessary to execute this tool, this can be installed via: + $npm install @nimiq/core-web@next + +Options: + --help Display this help page + --validator Path to the validator specification file (JSONC) + --network The network that should be used to register a validator. + If this argument is not provided, we connect to the test network by default + `); +} + +const START = Date.now(); +const argv = require('minimist')(process.argv.slice(2)); +const Nimiq = require('../../dist/node.js'); +const NimiqPOS = require ('@nimiq/core-web'); +const config = require('./modules/Config.js')(argv); +const fs = require('fs'); +const JSON5 = require('json5'); +const TXN_VALUE = 1; +const TXN_FEE = 0; + +// This tool uses a nano node +config.protocol = 'dumb'; +config.type = 'nano'; +config.network = 'test'; + +Nimiq.Log.instance.level = config.log.level; +for (const tag in config.log.tags) { + Nimiq.Log.instance.setLoggable(tag, config.log.tags[tag]); +} + +for (const key in config.constantOverrides) { + Nimiq.ConstantHelper.instance.set(key, config.constantOverrides[key]); +} + +for (const seedPeer of config.seedPeers) { + if (!seedPeer.host || !seedPeer.port) { + console.error('Seed peers must have host and port attributes set'); + process.exit(1); + } +} + +const TAG = 'Node'; +const $ = {}; + +(async () => { + if (argv.help) { + return help(); + } + + if (!argv.validator){ + + const pathToValidatorsFile = 'validator-keys.json'; + + // Check if the file or directory exists synchronously + if (fs.existsSync(pathToValidatorsFile)) { + console.log(`WARNING! \nThe validator keys file already exists`); + console.log(`\nPlease remove this file if you are sure you want to generate new validator keys`); + process.exit(0); + } + + // We are in validator configuration generation mode + // We need to generate the validator paramaters and exit the tool + console.log('Generating new validator paramaters... \n\n'); + + // First we generate the validator address parameters. + const validatorKeyPair = NimiqPOS.KeyPair.generate(); + const validatorAddress = validatorKeyPair.toAddress(); + + console.log(' Validator Account: '); + console.log('Address: '); + console.log(validatorAddress.toUserFriendlyAddress()); + console.log('Public key: '); + console.log(validatorKeyPair.publicKey.toHex()); + console.log('Private Key: '); + console.log(validatorKeyPair.privateKey.toHex()); + + // Now we generate the signing key. + const signingKeyPair = NimiqPOS.KeyPair.generate(); + + console.log('\n Signing key: '); + console.log('Public key: '); + console.log(signingKeyPair.publicKey.toHex()); + console.log('Private key: '); + console.log(signingKeyPair.privateKey.toHex()); + + // Finally we generate the voting key (BLS) + const votingKeyPair = NimiqPOS.BLSKeyPair.generate(); + + console.log('\n Voting BLS key: '); + console.log('Public key: '); + console.log(votingKeyPair.publicKey.toHex()); + console.log('Secret key: '); + console.log(votingKeyPair.secretKey.toHex()); + + // Now we write the configuration to a JSON file + const validatorConfiguration = { + 'ValidatorAccount': { + 'Address': validatorAddress.toUserFriendlyAddress(), + 'PublicKey': validatorKeyPair.publicKey.toHex(), + 'PrivateKey': validatorKeyPair.privateKey.toHex(), + }, + 'SigningKey': { + 'PublicKey': signingKeyPair.publicKey.toHex(), + 'PrivateKey': signingKeyPair.privateKey.toHex(), + }, + 'VotingKey': { + 'PublicKey': votingKeyPair.publicKey.toHex(), + 'SecretKey': votingKeyPair.secretKey.toHex(), + } + }; + + // converting the JSON object to a string + const data = JSON.stringify(validatorConfiguration); + + try { + fs.writeFileSync(pathToValidatorsFile, data); + } catch (error) { + // logging the error + console.error(error); + + throw error; + } + + console.log('\n\nValidator configuration file sucessfully written (validator-keys.json).'); + + process.exit(0); + } + + if (argv.network){ + config.network = argv.network + } + + console.log(' Reading validator configuration.. '); + let validator_config = JSON5.parse(fs.readFileSync(argv.validator)); + + console.log(validator_config); + + Nimiq.Log.i(TAG, `Nimiq NodeJS Client starting (network=${config.network}` + + `, ${config.host ? `host=${config.host}, port=${config.port}` : 'dumb'}`); + + Nimiq.GenesisConfig.init(Nimiq.GenesisConfig.CONFIGS[config.network]); + + for (const seedPeer of config.seedPeers) { + let address; + switch (seedPeer.protocol) { + case 'ws': + address = Nimiq.WsPeerAddress.seed(seedPeer.host, seedPeer.port, seedPeer.publicKey); + break; + case 'wss': + default: + address = Nimiq.WssPeerAddress.seed(seedPeer.host, seedPeer.port, seedPeer.publicKey); + break; + } + Nimiq.GenesisConfig.SEED_PEERS.push(address); + } + + const clientConfigBuilder = Nimiq.Client.Configuration.builder(); + clientConfigBuilder.protocol(config.protocol, config.host, config.port, config.tls.key, config.tls.cert); + if (config.reverseProxy.enabled) clientConfigBuilder.reverseProxy(config.reverseProxy.port, config.reverseProxy.header, ...config.reverseProxy.addresses); + if (config.passive) clientConfigBuilder.feature(Nimiq.Client.Feature.PASSIVE); + clientConfigBuilder.feature(Nimiq.Client.Feature.MEMPOOL); + + const clientConfig = clientConfigBuilder.build(); + const networkConfig = clientConfig.networkConfig; + + $.consensus = await (!config.volatile + ? Nimiq.Consensus.nano(networkConfig) + : Nimiq.Consensus.volatileNano(networkConfig)); + + $.client = new Nimiq.Client(clientConfig, $.consensus); + $.blockchain = $.consensus.blockchain; + $.accounts = $.blockchain.accounts; + $.mempool = $.consensus.mempool; + $.network = $.consensus.network; + + Nimiq.Log.i(TAG, `Peer address: ${networkConfig.peerAddress.toString()} - public key: ${networkConfig.keyPair.publicKey.toHex()}`); + + // This is the hardcoded address dedicated to validator registration + const recipientAddr = Nimiq.Address.fromUserFriendlyAddress('NQ07 0000 0000 0000 0000 0000 0000 0000 0000'); + + // Extract the validator configuration (we read the private keys from the config file) + let validatorPrivateKey = Nimiq.PrivateKey.unserialize(Nimiq.BufferUtils.fromHex(validator_config.ValidatorAccount.PrivateKey)); + let signingPrivateKey = Nimiq.PrivateKey.unserialize(Nimiq.BufferUtils.fromHex(validator_config.SigningKey.PrivateKey)); + let votingSecretKey = NimiqPOS.BLSSecretKey.fromHex(validator_config.VotingKey.SecretKey); + + // Create the KeyPairs + const validatorKeyPair = Nimiq.KeyPair.derive(validatorPrivateKey); + const SigningKeyPair = Nimiq.KeyPair.derive(signingPrivateKey); + const votingKeyPair = NimiqPOS.BLSKeyPair.derive(votingSecretKey); + + // Create a new validator registry + let validator = new ValidatorRegistry(validatorKeyPair.publicKey.toAddress(), SigningKeyPair.publicKey, votingKeyPair.publicKey); + + // Get the transaction's data + let data = validator.get_serialized_data(); + + // We monitor the status of the validator registration transactions + $.client.addTransactionListener((tx) => { + console.log(' Transaction update: '); + console.log(tx); + }, [recipientAddr]); + + let consensusState = Nimiq.Client.ConsensusState.CONNECTING; + $.client.addConsensusChangedListener(async (state) => { + consensusState = state; + if (state === Nimiq.Client.ConsensusState.ESTABLISHED) { + Nimiq.Log.i(TAG, `Blockchain ${config.type}-consensus established in ${(Date.now() - START) / 1000}s.`); + const chainHeight = await $.client.getHeadHeight(); + const chainHeadHash = await $.client.getHeadHash(); + Nimiq.Log.i(TAG, `Current state: height=${chainHeight}, headHash=${chainHeadHash}`); + + const account = await $.client.getAccount(validatorKeyPair.publicKey.toAddress()).catch(() => null); + const balance = Nimiq.Policy.lunasToCoins(account.balance) + + Nimiq.Log.i(TAG, `Validator address ${validatorKeyPair.publicKey.toAddress().toUserFriendlyAddress()}.` + + (account ? ` Balance: ${balance} NIM` : '')); + + // We need to send 6 txns, 1 Luna each + if (account.balance < 6){ + console.error('Not enough funds to pay the validator registration txns'); + process.exit(1); + } + + // Once we obtain consensus, we send the validator transactions + // Send the txns for validator registration + for (let i = 0; i < 6; i++) { + console.log('sending transaction: '); + + let transaction = new Nimiq.ExtendedTransaction(validatorKeyPair.publicKey.toAddress(), Nimiq.Account.Type.BASIC, recipientAddr, Nimiq.Account.Type.BASIC, TXN_VALUE, TXN_FEE, chainHeight, Nimiq.Transaction.Flag.NONE, data[i]); + + const proof = Nimiq.SignatureProof.singleSig(validatorKeyPair.publicKey, Nimiq.Signature.create(validatorKeyPair.privateKey, validatorKeyPair.publicKey, transaction.serializeContent())).serialize(); + transaction.proof = proof; + + let result = await $.client.sendTransaction(transaction); + + console.log(' Transaction result: '); + console.log(result); + + console.log(' Transaction hash: '); + console.log(transaction.hash().toHex()); + } + } + }); + + $.client.addBlockListener(async (hash) => { + if (consensusState === Nimiq.Client.ConsensusState.SYNCING) { + const head = await $.client.getBlock(hash, false); + if (head.height % 100 === 0) { + Nimiq.Log.i(TAG, `Syncing at block: ${head.height}`); + } + } + }); + + $.client.addHeadChangedListener(async (hash, reason) => { + const head = await $.client.getBlock(hash, false); + Nimiq.Log.i(TAG, `Now at block: ${head.height} (${reason})`); + }); + + $.network.on('peer-joined', (peer) => { + Nimiq.Log.i(TAG, `Connected to ${peer.peerAddress.toString()}`); + }); + $.network.on('peer-left', (peer) => { + Nimiq.Log.i(TAG, `Disconnected from ${peer.peerAddress.toString()}`); + }); +})().catch(e => { + console.error(e); + process.exit(1); +}); diff --git a/package.json b/package.json index cfc190a90..2cd22fce1 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ }, "gypfile": true, "dependencies": { + "@nimiq/core-web": "^2.0.0-alpha.20.3", "@nimiq/jungle-db": "^0.10.1", "atob": "^2.0.3", "bindings": "^1.3.0", diff --git a/yarn.lock b/yarn.lock index fd979ba16..5d5e02fdb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1048,6 +1048,14 @@ resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz#323d72dd25103d0c4fbdce89dadf574a787b1f9b" integrity sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ== +"@nimiq/core-web@^2.0.0-alpha.20.3": + version "2.0.0-alpha.20.3" + resolved "https://registry.yarnpkg.com/@nimiq/core-web/-/core-web-2.0.0-alpha.20.3.tgz#bfdab39f88da5ef8d83e5a8ec621214093d81b77" + integrity sha512-bH71nfWlWTE0S0nRjCyK3hCsBTZMROU8jVaoXtRUdGZx/GjyT7LQ4jx5nEWhw5tuPDVzH+dlvFGK7GGgftpXVw== + dependencies: + comlink "^4.4.1" + websocket "^1.0.34" + "@nimiq/jungle-db@^0.10.1": version "0.10.1" resolved "https://registry.yarnpkg.com/@nimiq/jungle-db/-/jungle-db-0.10.1.tgz#8468912c7ee4560e50d4bf4decced3779f6a544a" @@ -1948,6 +1956,13 @@ buffer@~5.2.1: base64-js "^1.0.2" ieee754 "^1.1.4" +bufferutil@^4.0.1: + version "4.0.8" + resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.8.tgz#1de6a71092d65d7766c4d8a522b261a6e787e8ea" + integrity sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw== + dependencies: + node-gyp-build "^4.3.0" + builtin-modules@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" @@ -2341,6 +2356,11 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" +comlink@^4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/comlink/-/comlink-4.4.1.tgz#e568b8e86410b809e8600eb2cf40c189371ef981" + integrity sha512-+1dlx0aY5Jo1vHy/tSsIGpSkN4tS9rZSW8FIhG0JH/crs9wwweswIo/POr451r7bZww3hFbPAKnTpimzL/mm4Q== + commander@^2.12.1, commander@^2.19.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" @@ -4748,7 +4768,7 @@ is-relative@^1.0.0: dependencies: is-unc-path "^1.0.0" -is-typedarray@~1.0.0: +is-typedarray@^1.0.0, is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== @@ -5814,6 +5834,11 @@ node-fetch@^2.6.1: dependencies: whatwg-url "^5.0.0" +node-gyp-build@^4.3.0: + version "4.8.0" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.0.tgz#3fee9c1731df4581a3f9ead74664369ff00d26dd" + integrity sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og== + node-gyp-build@^4.6.0: version "4.6.1" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.1.tgz#24b6d075e5e391b8d5539d98c7fc5c210cac8a3e" @@ -7731,6 +7756,13 @@ type@^2.7.2: resolved "https://registry.yarnpkg.com/type/-/type-2.7.2.tgz#2376a15a3a28b1efa0f5350dcf72d24df6ef98d0" integrity sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw== +typedarray-to-buffer@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" + integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== + dependencies: + is-typedarray "^1.0.0" + typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" @@ -7906,6 +7938,13 @@ use@^3.1.0: resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== +utf-8-validate@^5.0.2: + version "5.0.10" + resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.10.tgz#d7d10ea39318171ca982718b6b96a8d2442571a2" + integrity sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ== + dependencies: + node-gyp-build "^4.3.0" + util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -8098,6 +8137,18 @@ webidl-conversions@^3.0.0: resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== +websocket@^1.0.34: + version "1.0.34" + resolved "https://registry.yarnpkg.com/websocket/-/websocket-1.0.34.tgz#2bdc2602c08bf2c82253b730655c0ef7dcab3111" + integrity sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ== + dependencies: + bufferutil "^4.0.1" + debug "^2.2.0" + es5-ext "^0.10.50" + typedarray-to-buffer "^3.1.5" + utf-8-validate "^5.0.2" + yaeti "^0.0.6" + whatwg-url-compat@~0.6.5: version "0.6.5" resolved "https://registry.yarnpkg.com/whatwg-url-compat/-/whatwg-url-compat-0.6.5.tgz#00898111af689bb097541cd5a45ca6c8798445bf" @@ -8212,6 +8263,11 @@ y18n@^5.0.5: resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== +yaeti@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/yaeti/-/yaeti-0.0.6.tgz#f26f484d72684cf42bedfb76970aa1608fbf9577" + integrity sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug== + yallist@^3.0.2: version "3.1.1" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"