]> zoso.dev Git - libnemo.git/commitdiff
Fix and add validations to block signing etc
authorMiro Metsänheimo <miro@metsanheimo.fi>
Fri, 18 Oct 2019 17:57:23 +0000 (20:57 +0300)
committerMiro Metsänheimo <miro@metsanheimo.fi>
Fri, 18 Oct 2019 17:57:23 +0000 (20:57 +0300)
README.md
index.ts
lib/block-signer.ts
lib/nano-address.ts
lib/nano-converter.ts
package-lock.json
package.json
test/test.js

index d62491a13ce7cc2c21244d79b94660a2c8f3b044..9d072d06b6f856889093a852b6eca957cb7431ff 100644 (file)
--- 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
 
 ```
-<script src="https://unpkg.com/nanocurrency-web@1.0.3" type="text/javascript"></script>
+<script src="https://unpkg.com/nanocurrency-web@1.0.5" type="text/javascript"></script>
 <scrypt type="text/javascript">
        NanocurrencyWeb.wallet.generate(...);
 </script>
index c7e69418119d36a117041397b50164de6dc57f67..59836450ac5dfa999588c923061b62f58005bbd8 100644 (file)
--- 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)
        },
 
 }
index e02249ed3abbf009c42aff784b89a243befde469..e9e2bed90e11bc419445624a412e248370b0919d 100644 (file)
@@ -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
index 8f37cb9c3faf858ef8673e8f2d1be9879765f4ea..805738016c6266648fd731ee80257a3cff5ec2ed 100644 (file)
@@ -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
index 0ce45b4dcbe6c7e0f94654c00274f8dd9f14063f..096beb63d868ee7389199a8074e3161d69d8624d 100644 (file)
@@ -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`)
                }
        }
 
index 2a21c66fd956f8fc533fcde8b1e47f440249a54d..e64393fe9feb2ec7ffd1d435ba7ec79a5df5d526 100644 (file)
@@ -1,6 +1,6 @@
 {
        "name": "nanocurrency-web",
-       "version": "1.0.1",
+       "version": "1.0.5",
        "lockfileVersion": 1,
        "requires": true,
        "dependencies": {
index e4c5ba7770cf9eb1e7ae24db272434c70d5d00e3..e19e728e2667f1130e95f3baaabf665952b87cb3 100644 (file)
@@ -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 <miro@metsanheimo.fi>",
        "license": "MIT",
index 7d53c846497269ac6af3f8ddcec4fc911177e7dc..cdaed487f2d005917c10c26564bf3756333500ff 100644 (file)
@@ -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',