From a6159a3325bcb1e6911c351bf251f0edbf42ad6c Mon Sep 17 00:00:00 2001 From: =?utf8?q?Miro=20Mets=C3=A4nheimo?= Date: Fri, 18 Oct 2019 20:57:23 +0300 Subject: [PATCH] Fix and add validations to block signing etc --- README.md | 14 ++--- index.ts | 38 ++++++++++--- lib/block-signer.ts | 120 ++++++++++++++++++++++++++++++++++++++---- lib/nano-address.ts | 50 +++++++++++++++++- lib/nano-converter.ts | 4 +- package-lock.json | 2 +- package.json | 2 +- test/test.js | 10 ++-- 8 files changed, 205 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index d62491a..9d072d0 100644 --- a/README.md +++ b/README.md @@ -73,9 +73,6 @@ const data = { // Your current balance in RAW walletBalanceRaw: '18618869000000000000000000000000', - // From the pending transaction - fromAddress: 'nano_1e5aqegc1jb7qe964u4adzmcezyo6o146zb8hm6dft8tkp79za3sxwjym5rx', - // Your address toAddress: 'nano_3kyb49tqpt39ekc49kbej51ecsjqnimnzw1swxz4boix4ctm93w517umuiw8', @@ -85,10 +82,13 @@ const data = { // From wallet info frontier: '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', + // From the pending transaction + transactionHash: 'CBC911F57B6827649423C92C88C0C56637A4274FF019E77E24D61D12B5338783', + // From the pending transaction in RAW amountRaw: '7000000000000000000000000000000', - // Generate the work server-side or a DPOW service + // Generate the work server-side or with a DPOW service work: 'c5cf86de24b24419', } @@ -120,7 +120,7 @@ const data = { // The amount to send in RAW amountRaw: '2000000000000000000000000000000', - // Generate work on server-side or a DPOW service + // Generate work on server-side or with a DPOW service work: 'fbffed7c73b61367', } @@ -146,7 +146,7 @@ const data = { // Previous block, from account info frontier: '128106287002E595F479ACD615C818117FCB3860EC112670557A2467386249D4', - // Generate work on the server side or a DPOW service + // Generate work on the server side or with a DPOW service work: '0000000000000000', } @@ -168,7 +168,7 @@ const converted = converter.convert('1000000000000000000000000000000', 'RAW', 'N ### In web ``` - + NanocurrencyWeb.wallet.generate(...); diff --git a/index.ts b/index.ts index c7e6941..5983645 100644 --- a/index.ts +++ b/index.ts @@ -1,6 +1,6 @@ import AddressGenerator from './lib/address-generator' import AddressImporter, { Account, Wallet } from './lib/address-importer' -import BlockSigner, { TransactionBlock, RepresentativeBlock, SignedBlock } from './lib/block-signer' +import BlockSigner, { SendBlock, ReceiveBlock, RepresentativeBlock, SignedBlock } from './lib/block-signer' import BigNumber from 'bignumber.js' import NanoConverter from './lib/nano-converter' @@ -92,7 +92,8 @@ const block = { /** * Sign a send block with the input parameters * - * For a receive block, put your own address to the 'toAddress' property. + * For a send block, put your own address to the 'fromAddress' property and + * the recipient address to the 'toAddress' property. * All the NANO amounts should be input in RAW format. The addresses should be * valid Nano addresses. Fetch the current balance, frontier (previous block) and * representative address from the blockchain and generate work for the signature. @@ -105,10 +106,33 @@ const block = { * @param {SendBlock} data The data for the block * @param {string} privateKey Private key to sign the block */ - sign: (data: TransactionBlock, privateKey: string): SignedBlock => { - return blockSigner.sign(data, privateKey) + send: (data: SendBlock, privateKey: string): SignedBlock => { + return blockSigner.send(data, privateKey) }, + + /** + * Sign a receive block with the input parameters + * + * For a receive block, put your own address to the 'toAddress' property. + * All the NANO amounts should be input in RAW format. The addresses should be + * valid Nano addresses. Fetch the current balance, frontier (previous block) and + * representative address from the blockchain and generate work for the signature. + * Input the receive amount and transaction hash from the pending block. + * + * The return value of this function is ready to be published to the blockchain. + * + * NOTICE: Always fetch up-to-date account info from the blockchain + * before signing the block + * + * @param {ReceiveBlock} data The data for the block + * @param {string} privateKey Private key to sign the block + */ + receive: (data: ReceiveBlock, privateKey: string): SignedBlock => { + return blockSigner.receive(data, privateKey) + }, + + /** * Sign a representative change block with the input parameters * @@ -126,14 +150,14 @@ const block = { * */ representative: (data: RepresentativeBlock, privateKey: string): SignedBlock => { - const block: TransactionBlock = { + const block: SendBlock = { ...data, fromAddress: data.address, amountRaw: '0', - toAddress: 'nano_1111111111111111111111111111111111111111111111111111hifc8npp' // Burn address + toAddress: 'nano_1111111111111111111111111111111111111111111111111111hifc8npp', // Burn address } - return blockSigner.sign(block, privateKey) + return blockSigner.send(block, privateKey) }, } diff --git a/lib/block-signer.ts b/lib/block-signer.ts index e02249e..e9e2bed 100644 --- a/lib/block-signer.ts +++ b/lib/block-signer.ts @@ -1,6 +1,6 @@ import BigNumber from 'bignumber.js' -import Convert from './util/convert' import Ed25519 from './ed25519' +import Convert from './util/convert' import NanoAddress from './nano-address' import NanoConverter from './nano-converter' @@ -14,22 +14,110 @@ export default class BlockSigner { preamble = 0x6.toString().padStart(64, '0') - sign(data: TransactionBlock, privateKey: string): SignedBlock { + receive(data: ReceiveBlock, privateKey: string): SignedBlock { + const validateInputRaw = (input: string) => !!input && !isNaN(+input) + if (!validateInputRaw(data.walletBalanceRaw)) { + throw new Error('Invalid format in wallet balance') + } + + if (!validateInputRaw(data.amountRaw)) { + throw new Error('Invalid format in send amount') + } + + if (!this.nanoAddress.validateNanoAddress(data.toAddress)) { + throw new Error('Invalid toAddress') + } + + if (!this.nanoAddress.validateNanoAddress(data.representativeAddress)) { + throw new Error('Invalid representativeAddress') + } + + if (!data.transactionHash) { + throw new Error('No transaction hash') + } + + if (!data.frontier) { + throw new Error('No frontier') + } + + if (!data.work) { + throw new Error('No work') + } + + if (!privateKey) { + throw new Error('Please input the private key to sign the block') + } + + const balanceNano = NanoConverter.convert(data.walletBalanceRaw, 'RAW', 'NANO') + const amountNano = NanoConverter.convert(data.amountRaw, 'RAW', 'NANO') + const newBalanceNano = new BigNumber(balanceNano).plus(new BigNumber(amountNano)) + const newBalanceRaw = NanoConverter.convert(newBalanceNano, 'NANO', 'RAW') + const newBalanceHex = Convert.dec2hex(newBalanceRaw, 16).toUpperCase() + const account = this.nanoAddressToHexString(data.toAddress) + const link = data.transactionHash + const representative = this.nanoAddressToHexString(data.representativeAddress) + + const signatureBytes = this.ed25519.sign( + this.generateHash(this.preamble, account, data.frontier, representative, newBalanceHex, link), + Convert.hex2ab(privateKey)) + + return { + type: 'state', + account: data.toAddress, + previous: data.frontier, + representative: data.representativeAddress, + balance: newBalanceRaw, + link: link, + signature: Convert.ab2hex(signatureBytes), + work: data.work, + } + } + + send(data: SendBlock, privateKey: string): SignedBlock { + const validateInputRaw = (input: string) => !!input && !isNaN(+input) + if (!validateInputRaw(data.walletBalanceRaw)) { + throw new Error('Invalid format in wallet balance') + } + + if (!validateInputRaw(data.amountRaw)) { + throw new Error('Invalid format in send amount') + } + + if (!this.nanoAddress.validateNanoAddress(data.toAddress)) { + throw new Error('Invalid toAddress') + } + + if (!this.nanoAddress.validateNanoAddress(data.fromAddress)) { + throw new Error('Invalid fromAddress') + } + + if (!this.nanoAddress.validateNanoAddress(data.representativeAddress)) { + throw new Error('Invalid representativeAddress') + } + + if (!data.frontier) { + throw new Error('No frontier') + } + + if (!data.work) { + throw new Error('No work') + } + if (!privateKey) { throw new Error('Please input the private key to sign the block') } - const balance = NanoConverter.convert(data.walletBalanceRaw, 'RAW', 'NANO') - const amount = NanoConverter.convert(data.amountRaw, 'RAW', 'NANO') - const newBalance = new BigNumber(balance).minus(new BigNumber(amount)) - const rawBalance = NanoConverter.convert(newBalance, 'NANO', 'RAW') - const hexBalance = Convert.dec2hex(rawBalance, 16).toUpperCase() + const balanceNano = NanoConverter.convert(data.walletBalanceRaw, 'RAW', 'NANO') + const amountNano = NanoConverter.convert(data.amountRaw, 'RAW', 'NANO') + const newBalanceNano = new BigNumber(balanceNano).minus(new BigNumber(amountNano)) + const newBalanceRaw = NanoConverter.convert(newBalanceNano, 'NANO', 'RAW') + const newBalanceHex = Convert.dec2hex(newBalanceRaw, 16).toUpperCase() const account = this.nanoAddressToHexString(data.fromAddress) const link = this.nanoAddressToHexString(data.toAddress) const representative = this.nanoAddressToHexString(data.representativeAddress) const signatureBytes = this.ed25519.sign( - this.generateHash(this.preamble, account, data.frontier, representative, hexBalance, link), + this.generateHash(this.preamble, account, data.frontier, representative, newBalanceHex, link), Convert.hex2ab(privateKey)) return { @@ -37,8 +125,8 @@ export default class BlockSigner { account: data.fromAddress, previous: data.frontier, representative: data.representativeAddress, - balance: rawBalance, - link, + balance: newBalanceRaw, + link: link, signature: Convert.ab2hex(signatureBytes), work: data.work, } @@ -74,7 +162,17 @@ export default class BlockSigner { } -export interface TransactionBlock { +export interface ReceiveBlock { + walletBalanceRaw: string + toAddress: string + transactionHash: string + frontier: string + representativeAddress: string + amountRaw: string + work: string +} + +export interface SendBlock { walletBalanceRaw: string fromAddress: string toAddress: string diff --git a/lib/nano-address.ts b/lib/nano-address.ts index 8f37cb9..8057380 100644 --- a/lib/nano-address.ts +++ b/lib/nano-address.ts @@ -74,11 +74,59 @@ export default class NanoAddress { return output } + /** + * Validates a Nano address with 'nano' and 'xrb' prefixes + * + * Derived from https://github.com/alecrios/nano-address-validator + * + * @param {string} address Nano address + */ + validateNanoAddress = (address: string): boolean => { + /** Ensure the address is provided */ + if (address === undefined) { + throw Error('Address must be defined.') + } + + /** Ensure the address is a string */ + if (typeof address !== 'string') { + throw TypeError('Address must be a string.') + } + + /** The array of allowed prefixes */ + const allowedPrefixes: string[] = ['nano', 'xrb'] + + /** The regex pattern for validating the address */ + const pattern = new RegExp( + `^(${allowedPrefixes.join('|')})_[13]{1}[13456789abcdefghijkmnopqrstuwxyz]{59}$`, + ) + + /** Validate the syntax of the address */ + if (!pattern.test(address)) return false + + /** The expected checksum as a base32-encoded string */ + const expectedChecksum = address.slice(-8) + + /** The public key as a base32-encoded string */ + const publicKey = address.slice(address.indexOf('_') + 1, -8) + + /** The public key as an array buffer */ + const publicKeyBuffer = this.decodeNanoBase32(publicKey) + + /** The actual checksum as an array buffer */ + const actualChecksumBuffer = blake2b(publicKeyBuffer, null, 5).reverse() + + /** The actual checksum as a base32-encoded string */ + const actualChecksum = this.encodeNanoBase32(actualChecksumBuffer) + + /** Validate the provided checksum against the derived checksum */ + return expectedChecksum === actualChecksum + } + readChar(char: string): number { const idx = this.alphabet.indexOf(char) if (idx === -1) { - throw `Invalid character found: ${char}` + throw new Error(`Invalid character found: ${char}`) } return idx diff --git a/lib/nano-converter.ts b/lib/nano-converter.ts index 0ce45b4..096beb6 100644 --- a/lib/nano-converter.ts +++ b/lib/nano-converter.ts @@ -27,7 +27,7 @@ export default class NanoConverter { value = value.shiftedBy(24) break default: - throw `Unkown input unit ${inputUnit}, expected one of the following: RAW, NANO, MRAI, KRAI, RAI` + throw new Error(`Unkown input unit ${inputUnit}, expected one of the following: RAW, NANO, MRAI, KRAI, RAI`) } switch (outputUnit) { @@ -41,7 +41,7 @@ export default class NanoConverter { case 'RAI': return value.shiftedBy(-24).toFixed(9, 1) default: - throw `Unknown output unit ${outputUnit}, expected one of the following: RAW, NANO, MRAI, KRAI, RAI` + throw new Error(`Unknown output unit ${outputUnit}, expected one of the following: RAW, NANO, MRAI, KRAI, RAI`) } } diff --git a/package-lock.json b/package-lock.json index 2a21c66..e64393f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "nanocurrency-web", - "version": "1.0.1", + "version": "1.0.5", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index e4c5ba7..e19e728 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nanocurrency-web", - "version": "1.0.4", + "version": "1.0.5", "description": "Toolkit for Nano cryptocurrency client side offline integrations", "author": "Miro Metsänheimo ", "license": "MIT", diff --git a/test/test.js b/test/test.js index 7d53c84..cdaed48 100644 --- a/test/test.js +++ b/test/test.js @@ -117,20 +117,20 @@ describe('derive more accounts from the same seed test', () => { describe('block signing tests using official test vectors', () => { it('should create a valid signature for a receive block', () => { - const result = block.sign({ + const result = block.receive({ walletBalanceRaw: '18618869000000000000000000000000', - fromAddress: 'nano_1e5aqegc1jb7qe964u4adzmcezyo6o146zb8hm6dft8tkp79za3sxwjym5rx', - toAddress: 'nano_3kyb49tqpt39ekc49kbej51ecsjqnimnzw1swxz4boix4ctm93w517umuiw8', + transactionHash: 'CBC911F57B6827649423C92C88C0C56637A4274FF019E77E24D61D12B5338783', + toAddress: 'nano_1e5aqegc1jb7qe964u4adzmcezyo6o146zb8hm6dft8tkp79za3sxwjym5rx', representativeAddress: 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou', frontier: '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', amountRaw: '7000000000000000000000000000000', work: 'c5cf86de24b24419', }, '781186FB9EF17DB6E3D1056550D9FAE5D5BBADA6A6BC370E4CBB938B1DC71DA3') - expect(result.signature.toUpperCase()).to.equal('EEFFE1EFCCC8F2F6F2F1B79B80ABE855939DD9D6341323186494ADEE775DAADB3B6A6A07A85511F2185F6E739C4A54F1454436E22255A542ED879FD04FEED001') + expect(result.signature.toUpperCase()).to.equal('F25D751AD0379A5718E08F3773DA6061A9E18842EF5615163C7F207B804CC2C5DD2720CFCE5FE6A78E4CC108DD9CAB65051526403FA2C24A1ED943BB4EA7880B') }) it('should create a valid signature for a send block', () => { - const result = block.sign({ + const result = block.send({ walletBalanceRaw: '5618869000000000000000000000000', fromAddress: 'nano_1e5aqegc1jb7qe964u4adzmcezyo6o146zb8hm6dft8tkp79za3sxwjym5rx', toAddress: 'nano_1q3hqecaw15cjt7thbtxu3pbzr1eihtzzpzxguoc37bj1wc5ffoh7w74gi6p', -- 2.34.1