From 32608e270fdd88fbbca25d6811fe983d609a64fa Mon Sep 17 00:00:00 2001 From: "Amir.M A" <33460495+Amir-m-a@users.noreply.github.com> Date: Sat, 11 May 2024 21:35:11 +0330 Subject: [PATCH 1/3] Add request type in broadcast message Signed-off-by: Amir m. Aghapour --- src/networking/broadcast.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/networking/broadcast.js b/src/networking/broadcast.js index 18c808f..479305b 100644 --- a/src/networking/broadcast.js +++ b/src/networking/broadcast.js @@ -4,7 +4,8 @@ const buffer = require('buffer/').Buffer; const Networking = { // Broadcast a rawTxBuffer to a sentinel at specified host {hostURL} and port {port} number // make sure to include the size of the rawTxBuffer before sending to the sentinel - broadcastTx: (port, host, signedTxHex) => { + // reqType indicates execute transaction (0) or validate transaction (1) + broadcastTx: (port, host, signedTxHex, reqType = 0) => { const client = new net.Socket(); @@ -13,18 +14,18 @@ const Networking = { const requestId = Math.round(Math.random() * 10000000); const reqIdBuf = buffer.alloc(8); reqIdBuf.writeBigUInt64LE(BigInt(requestId)); + const reqTypeBuf = buffer.alloc(1); + reqTypeBuf.writeUInt8(reqType); const sizePacket = buffer.alloc(8); const signedTxBuffer = buffer.from(signedTxHex, 'hex'); - sizePacket.writeBigUInt64LE(BigInt(signedTxBuffer.length + reqIdBuf.length)); - const finalPacket = buffer.concat([sizePacket, reqIdBuf, signedTxBuffer]); - - client.write(finalPacket, (err) => { - console.log('Error', err); - }); + sizePacket.writeBigUInt64LE(BigInt(signedTxBuffer.length + reqIdBuf.length + reqTypeBuf.length)); + const finalPacket = buffer.concat([sizePacket, reqIdBuf, reqTypeBuf, signedTxBuffer]); + client.write(finalPacket); }); client.on('data', (data) => { console.log('Received: ' + data.toString('hex')); + // TODO: read response completely (based on packet length) to prevent killing prematurely client.destroy(); // kill client after server's response }); From 2347a948942028154140a97ac367f462a470beca Mon Sep 17 00:00:00 2001 From: "Am.A" Date: Tue, 14 May 2024 10:20:24 +0330 Subject: [PATCH 2/3] compact tx added response of broadcast handled full example for mint and transfer added Signed-off-by: Amir m. Aghapour --- README.md | 81 +++++++++--- package.json | 2 +- src/crypto/publickey.js | 19 ++- src/crypto/secretkey.js | 2 +- src/crypto/utils.js | 18 +-- src/networking/broadcast.js | 80 ++++++----- src/transaction/input.js | 1 + src/transaction/transaction.js | 227 ++++++++++++++++++++++---------- test/transaction/transaction.js | 29 +++- 9 files changed, 317 insertions(+), 142 deletions(-) diff --git a/README.md b/README.md index ba36703..2b9ee7b 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ The following is a breakdown of sections and classes for this cbdc-module: - constructor(script_type, pubHex) - ```a new Address Object``` - script_type - {Number} representing the script type of the address - pubHex - {String} represents valid 32 byte hexadecimal string - - getAddress() - returns {String} ```represented the bech32 encoding version of a publickey e.g. ``` + - getAddress() - returns {String} ```represented the bech32 encoding version of a publickey ``` - decodeAddress() - returns {Object} ```with fields script_type (string representing) and pubHex (hex string of public key)``` - static decodeFromAddressString(address) - returns ```{Object} with fields script_type {Number} and pubHex {String} (hexdecimal string of public key)``` @@ -40,7 +40,9 @@ The following is a breakdown of sections and classes for this cbdc-module: - secretKeyData - ```{String} a valid 32 byte hexadecimal string``` - static fromPrivateKeyData(secretKeyData) - secretKeyData - ```{String} a valid 32 byte hexadecimal string``` - - toBuffer() - ```returns {Buffer} 32 byte buffer publickey``` + - getWitnessCommit(scriptType) - ```returns witness commitment``` + - scriptType - ```{string} hex representation of script type ("00" for P2PK)``` + - toBuffer() - ```returns {Buffer} 32 byte buffer publickey``` - Secretkey - contructor(randBytes) - ```returns new SecretKey with randBytes provided as random seed (if provided), otherwise it creates random key from random secrety bytes``` @@ -54,10 +56,14 @@ The following is a breakdown of sections and classes for this cbdc-module: ## Networking * Networking - - broadcastTx(port, host, signedTxHex) - ```broadcast a signedTxBuf to a sentinel server at host {host} and port {port} number``` + - broadcast(port, host, payloadHex, reqType) - ```Broadcast a payload to a (sentinel/coordinator/shard) and returns a promise that resolves to response``` - port - {number} port number of host - - host - {string} hostname url to send signedTx - - signedTxHex - {string} a valid hexadecimal encoded transaction that has been signed + - host - {string} hostname or url to send payloadHex + - payloadHex - {string} a valid hexadecimal encoded message, could be signed tx or compact tx or any other message + - reqType - {number|null} indicates type of request. can be null for no request type and for others: + - sentinel: 0=execute, 1=validate + - shard (read-only endpoint): 0=UHS, 1=tx + - coordinator doesn't have request type ## Transaction - Input @@ -67,20 +73,9 @@ The following is a breakdown of sections and classes for this cbdc-module: - witnessProgramCommitment {String} - witness commitment for this input - value {number} - the number of dollar units this input is worth - writeInputToBuffer() - ```returns Buffer representation as buffer type``` - - getUHSHash() - ```returns {Buffer} Universal Hash Set hash of the input e.g. concatentation of [txid, index, witnessProgramCommitment, value] into bytes``` - e.g. + - getUHSHash() - ```returns {Buffer} Universal Hash Set hash of the input i.e. concatenation of [txid, index, witnessProgramCommitment, value] into bytes``` - toString() - ```returns {String} representing valid input``` -## Utils -- Utility Methods CBDC module - - sign(secretKey, message) - ```returns signature that signed message {message} with privateKey {privateKey}``` - - secretKey - {String} - 32 byte hexadecimal string - - message - {String} - message that is signed - - verify(publicKey, message, signature) ```returns true or false whether the produced signature is validly signed publicKey``` - - publicKey - 32 byte hexadecimal string - - message - message to verify was signed - - signature - {String} signature to verify validly signed message against public key message pair - *N.B.:* A hash is used to identify a specfic UTXO within the monetary supply (the UHS ID); it is a concatenation of a txid, a 64-bit index of the UTXO's position in previous tx's outputs, a witnessProgramCommitment, and the 64-bit encoded value of that output - Output @@ -99,11 +94,23 @@ The following is a breakdown of sections and classes for this cbdc-module: - outputs - Array{Output} - witnesses - Array of witness object - toHex() - ```returns {String} hexadecimal string of the unsigned raw transaction``` + - getCompactHex(sentinelAttestation) - ```returns {String} compact-tx in hexadecimal format with given sentinel attestations``` - getTxid() - ```returns {String} returns hexadecimal string of transaction id (txid)``` - sign(secretKey) - ```returns {Buffer} signed tx in bytes``` - static txFromHex(rawHex) - ```returns {Transaction} object from the provided rawTx``` - rawHex - valid hexadecimal string represents a valid tx +## Utils +- Utility Methods CBDC module + - sign(secretKey, message) - ```returns signature that signed message {message} with privateKey {privateKey}``` + - secretKey - {String} - 32 byte hexadecimal string + - message - {String} - message that is signed + - verify(publicKey, message, signature) ```returns true or false whether the produced signature is validly signed publicKey``` + - publicKey - 32 byte hexadecimal string + - message - message to verify was signed + - signature - {String} signature to verify validly signed message against public key message pair + + ## Special Notes BigInt is used everywhere throughout this module, that may pose problems in using toJson() methods or serializing for output @@ -137,7 +144,7 @@ The following are example code snippets. ```js let network = require('./networking/broadcast'); const txBuf = new Transaction(); - Networking.broadcastTx(5555, '127.0.0.1', Buffer.from(tx.toHex(), 'hex')) + Networking.broadcast(5555, '127.0.0.1', Buffer.from(tx.toHex(), 'hex')) ``` - Transaction ```js @@ -153,7 +160,7 @@ The following are example code snippets. 01000000000000009f981e64afc0fc56a0d7b355cd9eba36f3d19507088713b1f73afc5bf301a44e000000000000000070cd87ebaaa0d2d059dccaceeb7f9f823a5791d60b00aef9d9573f1fbf91ca29c800000000000000010000000000000081b095a242974d9f4e98ca18b468b8e644e4168380a035b3d66bc279b36c6510c80000000000000001000000000000006100000000000000003ad8f015f9212f8262248af4cf4cc39907d0215fdde14507f8bc09ad5836bbe901986cc97272bdb7624a824afcef76936b8f945e55d6e8479b95c81298e77b42d5a255e8529fc2f0d90743e7f9997a7159b6121105c7ec9b9252da992f34611f ``` - - Constructing, signing and broacasting tx to sentinel + - Constructing, signing and broadcasting tx to sentinel for execution ```js const secretKey = 'e00b5c3d80899217a22fea87e7337907203df8a1efebd4d2a8773c8f629fff36'; @@ -163,7 +170,7 @@ The following are example code snippets. tx.sign(secretKey); - Networking.broadcastTx(5555, '127.0.0.1', Buffer.from(tx.toHex(), 'hex')) + Networking.broadcast(5555, '127.0.0.1', Buffer.from(tx.toHex(), 'hex')) ``` - Signing and Verifying @@ -176,6 +183,40 @@ The following are example code snippets. const sig = utils.sign(secretKey, message); utils.verify(publicKey, Buffer.from(sha256(message), 'hex'), sig); ``` + +- Full example for minting and transferring in **2PC** architecture + ```js + const user1_sk = new Secretkey(); + const user2_sk = new Secretkey(); + const user1_pk = new Publickey(user1_sk.secretKeyBuf); + const user2_pk = new Publickey(user2_sk.secretKeyBuf); + const witness1 = user1_pk.getWitnessCommit(); + const witness2 = user2_pk.getWitnessCommit(); + + const sentinel_sk = "SENTINEL_SECRET_KEY_HEX"; + const sentinel_pk = Publickey.fromPrivateKeyData(sentinel_sk); + + const output1 = new Output(witness1, 50); + const mint_tx = new Transaction([], [output1], []); + const input = new Input(mint_tx.getTxid(), 0, witness1, 50); + const compactMintTx = buffer.from(mint_tx.getCompactHex([]), 'hex'); + const sentinelAttestation = buffer.concat([ + sentinel_pk, + schnorr.sign(sentinel_sk, buffer.from(Utils.sha256(compactMintTx), 'hex')), + ]); + console.log(await Comms.broadcast(SHARD_PORT, SHARD_IP, input.getUHSHash(), 0)); // input UHS doesn't exists, so output will be 0100 + console.log(await Comms.broadcast(COORD_PORT, COORD_IP, mint_tx.getCompactHex([sentinel_attest]), null)); // okay mint result will be 0101 + console.log(await Comms.broadcast(SHARD_PORT, SHARD_IP, input.getUHSHash(), 0)); // input UHS will exist and output will be 0101 + + const output22 = new Output(witness2, 20); // pay 20 to user2 + const output21 = new Output(witness1, 30); // take 30 back as change + const transferTx = new Transaction([input], [output22, output21], []); + const input21 = new Input(transferTx.getTxid(), 1, witness1, 30); + transferTx.sign(user1_sk.toHex()); + console.log(await Comms.broadcast(SHARD_PORT, SHARD_IP, input21.getUHSHash(), 0)); // input21 UHS doesn't exists, so output will be 0100 + console.log(await Comms.broadcast(SENTINEL_PORT, SENTINEL_IP, transferTx.toHex(), 0)); // output for successful transfer = 01 00 03 00 00 00 00 + console.log(await Comms.broadcast(SHARD_PORT, SHARD_IP, input21.getUHSHash(), 0)); // input21 UHS exists after successful transfer, so output will be 0101 + ``` ## Sample Data diff --git a/package.json b/package.json index 4253d1c..22c3c53 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@mit-dci/opencbdc", "version": "0.0.2", - "description": "An javascript module for interacting with the opencbdc-tx atomizer and 2pc environments", + "description": "A javascript module for interacting with the opencbdc-tx atomizer and 2pc environments", "main": "index.js", "scripts": { "test": "mocha test/**/*.js", diff --git a/src/crypto/publickey.js b/src/crypto/publickey.js index 0b5d0af..994b196 100644 --- a/src/crypto/publickey.js +++ b/src/crypto/publickey.js @@ -1,25 +1,35 @@ const Secp256k1 = require('@enumatech/secp256k1-js'); const buffer = require('buffer/').Buffer; +const utils = require('./utils'); class PublicKey { - /** * @param secretKeyData privateKey hex string */ constructor(secretKeyData) { this.secretKeyBuf = buffer.from(secretKeyData, 'hex'); - this.publicKey = Secp256k1.generatePublicKeyFromPrivateKeyData(Secp256k1.uint256(secretKeyData, 16)).x; + this.publicKey = Secp256k1.generatePublicKeyFromPrivateKeyData( + Secp256k1.uint256(secretKeyData, 16), + ).x; } /** * @param {string} secretKeyBuf - * @returns {Buffer} PublicKey Buffer + * @returns {Buffer} PublicKey Buffer */ static fromPrivateKeyData(secretKeyData) { const pubHex = Secp256k1.generatePublicKeyFromPrivateKeyData(Secp256k1.uint256(secretKeyData, 16)).x; return buffer.from(pubHex, 'hex'); } - + + /** + * @param {string} scriptType hex representation of script type (0 for P2PK) + * @returns {Buffer} witness commitment + */ + getWitnessCommit(scriptType = '00') { + return buffer.from(utils.sha256(buffer.from(scriptType + this.publicKey, 'hex')), 'hex'); + } + /** * @returns {Buffer} representing a public key in size x bytes */ @@ -29,4 +39,3 @@ class PublicKey { } module.exports = PublicKey; - diff --git a/src/crypto/secretkey.js b/src/crypto/secretkey.js index 9177808..07c3582 100644 --- a/src/crypto/secretkey.js +++ b/src/crypto/secretkey.js @@ -40,7 +40,7 @@ class SecretKey { * @returns {string} hexadecimal version of private key */ toHex() { - return Buffer.toString(this.secretKeyBuf, 'hex'); + return this.secretKeyBuf.toString('hex'); } } diff --git a/src/crypto/utils.js b/src/crypto/utils.js index 064c942..66f95a4 100644 --- a/src/crypto/utils.js +++ b/src/crypto/utils.js @@ -1,13 +1,15 @@ const schnorr = require('bip-schnorr'); +const crypto = require('crypto'); /** * Return sha256 of input b - * @param {*} b - * @returns + * @param {*} b + * @returns hex SHA256 digest */ - -const sha256 = function a(b){function c(a,b){return a>>>b|a<<32-b;}for(var d,e,f=Math.pow,g=f(2,32),h='length',i='',j=[],k=8*b[h],l=a.h=a.h||[],m=a.k=a.k||[],n=m[h],o={},p=2;64>n;p++)if(!o[p]){for(d=0;313>d;d+=p)o[d]=p;l[n]=f(p,.5)*g|0,m[n++]=f(p,1/3)*g|0;}for(b+='\x80';b[h]%64-56;)b+='\x00';for(d=0;d>8)return;j[d>>2]|=e<<(3-d)%4*8;}for(j[j[h]]=k/g|0,j[j[h]]=k,e=0;ed;d++){var s=q[d-15],t=q[d-2],u=l[0],v=l[4],w=l[7]+(c(v,6)^c(v,11)^c(v,25))+(v&l[5]^~v&l[6])+m[d]+(q[d]=16>d?q[d]:q[d-16]+(c(s,7)^c(s,18)^s>>>3)+q[d-7]+(c(t,17)^c(t,19)^t>>>10)|0),x=(c(u,2)^c(u,13)^c(u,22))+(u&l[1]^u&l[2]^l[1]&l[2]);l=[w+x|0].concat(l),l[4]=l[4]+w|0;}for(d=0;8>d;d++)l[d]=l[d]+r[d]|0;}for(d=0;8>d;d++)for(e=3;e+1;e--){var y=l[d]>>8*e&255;i+=(16>y?0:'')+y.toString(16);} - return i;}; + +const sha256 = (msg) => { + return crypto.createHash('sha256').update(Buffer.from(msg)).digest('hex'); +}; /** * Returns signature that signed message {message} with privateKey {privateKey} @@ -34,7 +36,7 @@ const verify = (publicKey, message, signature) => { try { schnorr.verify(pubkeyBuf, messageBuf, sigBuf); } catch (error) { - console.log('Error msg: ', error); + console.log('Error msg: ', error); return false; } return true; @@ -43,5 +45,5 @@ const verify = (publicKey, message, signature) => { module.exports = { sign, sha256, - verify -}; \ No newline at end of file + verify, +}; diff --git a/src/networking/broadcast.js b/src/networking/broadcast.js index 479305b..4b5d2ec 100644 --- a/src/networking/broadcast.js +++ b/src/networking/broadcast.js @@ -2,39 +2,57 @@ const net = require('net'); const buffer = require('buffer/').Buffer; const Networking = { - // Broadcast a rawTxBuffer to a sentinel at specified host {hostURL} and port {port} number - // make sure to include the size of the rawTxBuffer before sending to the sentinel - // reqType indicates execute transaction (0) or validate transaction (1) - broadcastTx: (port, host, signedTxHex, reqType = 0) => { - - const client = new net.Socket(); - - client.connect(port, host, () => { - console.log('Connected'); - const requestId = Math.round(Math.random() * 10000000); - const reqIdBuf = buffer.alloc(8); - reqIdBuf.writeBigUInt64LE(BigInt(requestId)); - const reqTypeBuf = buffer.alloc(1); + /** + * Broadcast a payload to a (sentinel/coordinator/shard) + * @param {number} port tcp port of destination + * @param {string} host ip address or hostname of destination + * @param {string} payloadHex hex representation of payload, could be tx or compact tx or any other message + * @param {(number|null)} reqType indicates type of request. can be null for no request type. + * - for sentinel 0=execute, 1=validate + * - for shard 0=UHS, 1=tx + * - coordinator doesn't have request type + * @return {Promise} response from destination + */ + broadcast: (port, host, payloadHex, reqType = 0) => { + const requestId = Math.round(Math.random() * Math.pow(2, 64)); + const reqIdBuf = buffer.alloc(8); + reqIdBuf.writeBigUInt64LE(BigInt(requestId)); + let reqTypeBuf; + if (reqType === null) { + // no request type in packet (when sending to coordinator) + reqTypeBuf = buffer.alloc(0); + } else { + reqTypeBuf = buffer.alloc(1); reqTypeBuf.writeUInt8(reqType); - const sizePacket = buffer.alloc(8); - const signedTxBuffer = buffer.from(signedTxHex, 'hex'); - sizePacket.writeBigUInt64LE(BigInt(signedTxBuffer.length + reqIdBuf.length + reqTypeBuf.length)); - const finalPacket = buffer.concat([sizePacket, reqIdBuf, reqTypeBuf, signedTxBuffer]); - client.write(finalPacket); - }); - - client.on('data', (data) => { - console.log('Received: ' + data.toString('hex')); - // TODO: read response completely (based on packet length) to prevent killing prematurely - client.destroy(); // kill client after server's response - }); - - client.on('close', () => { - console.log('Connection closed'); - }); + } + const payloadBuffer = buffer.from(payloadHex, 'hex'); + const sizePacket = buffer.alloc(8); + sizePacket.writeBigUInt64LE(BigInt(payloadBuffer.length + reqIdBuf.length + reqTypeBuf.length)); + const finalPacket = buffer.concat([sizePacket, reqIdBuf, reqTypeBuf, payloadBuffer]); - client.on('error', (err) => { - console.error(err); + return new Promise((resolve, reject) => { + const client = new net.Socket(); // TODO: use single socket per class instance + client.setTimeout(5000, client.kill); // 5s timeout + client.on('error', reject); + client.on('timeout', reject); + client.on('close', reject); + client.connect(port, host, () => { + let packetLength; + let receivedData = buffer.alloc(0); + client.on('data', (chunk) => { + receivedData = Buffer.concat([receivedData, chunk]); + if (packetLength === undefined && receivedData.length >= 8) { + packetLength = receivedData.readBigUInt64LE(); + receivedData = receivedData.subarray(8); + } + if (packetLength !== undefined && receivedData.length >= packetLength) { + resolve(receivedData.subarray(8)); // ignore request id + client.destroy(); // kill client after server's complete response + } + client.read(); // poll for any buffered data + }); + client.write(finalPacket); + }); }); }, }; diff --git a/src/transaction/input.js b/src/transaction/input.js index fb0364f..ef617ab 100644 --- a/src/transaction/input.js +++ b/src/transaction/input.js @@ -1,4 +1,5 @@ const buffer = require('buffer/').Buffer; +const crypto = require('crypto'); /* * Input class represents the 'input' abstraction in digital currency transaction diff --git a/src/transaction/transaction.js b/src/transaction/transaction.js index 198e385..adb6b61 100644 --- a/src/transaction/transaction.js +++ b/src/transaction/transaction.js @@ -7,7 +7,7 @@ const Secp256k1 = require('@enumatech/secp256k1-js'); class Transaction { /** - * + * * @param {Array(Inputs)} inputs Array(Input) of Inputs objects * @param {Array(Outputs)} outputs Array(Outputs) of Output Objects * @param {Array} Array of Witness Object data for Transaction (req: should be ordered according to input data) @@ -27,21 +27,24 @@ class Transaction { let ary = []; // inputs - if (!'inputs' in this || !Array.isArray(this.inputs)) - throw new Error('object doesn\'t contain inputs'); + if ((!'inputs') in this || !Array.isArray(this.inputs)) + throw new Error("object doesn't contain inputs"); const inbuf = buffer.alloc(8); inbuf.writeBigUInt64LE(BigInt(this.inputs.length)); ary.push(inbuf); - for (let i=0; i Date: Sat, 18 May 2024 17:33:57 +0330 Subject: [PATCH 3/3] fix and added data type checks Signed-off-by: Amir m. Aghapour --- README.md | 2 +- src/crypto/publickey.js | 4 +- src/crypto/test-utils.js | 5 +- src/crypto/utils.js | 15 +- src/transaction/transaction.js | 300 +++++++++++++++++--------------- test/transaction/transaction.js | 22 ++- 6 files changed, 190 insertions(+), 158 deletions(-) diff --git a/README.md b/README.md index 2b9ee7b..13e43d2 100644 --- a/README.md +++ b/README.md @@ -205,7 +205,7 @@ The following are example code snippets. schnorr.sign(sentinel_sk, buffer.from(Utils.sha256(compactMintTx), 'hex')), ]); console.log(await Comms.broadcast(SHARD_PORT, SHARD_IP, input.getUHSHash(), 0)); // input UHS doesn't exists, so output will be 0100 - console.log(await Comms.broadcast(COORD_PORT, COORD_IP, mint_tx.getCompactHex([sentinel_attest]), null)); // okay mint result will be 0101 + console.log(await Comms.broadcast(COORD_PORT, COORD_IP, mint_tx.getCompactHex([sentinelAttestation]), null)); // okay mint result will be 0101 console.log(await Comms.broadcast(SHARD_PORT, SHARD_IP, input.getUHSHash(), 0)); // input UHS will exist and output will be 0101 const output22 = new Output(witness2, 20); // pay 20 to user2 diff --git a/src/crypto/publickey.js b/src/crypto/publickey.js index 994b196..87ae2b0 100644 --- a/src/crypto/publickey.js +++ b/src/crypto/publickey.js @@ -24,10 +24,10 @@ class PublicKey { /** * @param {string} scriptType hex representation of script type (0 for P2PK) - * @returns {Buffer} witness commitment + * @returns {string} witness commitment in hex */ getWitnessCommit(scriptType = '00') { - return buffer.from(utils.sha256(buffer.from(scriptType + this.publicKey, 'hex')), 'hex'); + return utils.sha256(buffer.from(scriptType + this.publicKey, 'hex')); } /** diff --git a/src/crypto/test-utils.js b/src/crypto/test-utils.js index 54a1a56..0ed8369 100644 --- a/src/crypto/test-utils.js +++ b/src/crypto/test-utils.js @@ -1,4 +1,5 @@ const utils = require('./utils'); +const buffer = require('buffer/').Buffer; const secretKey = 'e00b5c3d80899217a22fea87e7337907203df8a1efebd4d2a8773c8f629fff36'; const publicKey = '3ad8f015f9212f8262248af4cf4cc39907d0215fdde14507f8bc09ad5836bbe9'; @@ -6,7 +7,7 @@ const message = 'onomatopoeia'; const sig = utils.sign(secretKey, message); -console.log('Pubkey Buffer: ', Buffer.from(publicKey, 'hex')); +console.log('Pubkey Buffer: ', buffer.from(publicKey, 'hex')); console.log('Signature', sig, sig.length); -console.log(utils.verify(publicKey, Buffer.from(utils.sha256(message), 'hex'), sig)); \ No newline at end of file +console.log(utils.verify(publicKey, buffer.from(utils.sha256(message), 'hex'), sig)); \ No newline at end of file diff --git a/src/crypto/utils.js b/src/crypto/utils.js index 66f95a4..090af50 100644 --- a/src/crypto/utils.js +++ b/src/crypto/utils.js @@ -1,14 +1,14 @@ const schnorr = require('bip-schnorr'); const crypto = require('crypto'); +const buffer = require('buffer/').Buffer; /** - * Return sha256 of input b - * @param {*} b + * Return sha256 of input + * @param {*} msg * @returns hex SHA256 digest */ - const sha256 = (msg) => { - return crypto.createHash('sha256').update(Buffer.from(msg)).digest('hex'); + return crypto.createHash('sha256').update(buffer.from(msg)).digest('hex'); }; /** @@ -19,6 +19,7 @@ const sha256 = (msg) => { */ const sign = (secretKey, message) => { const messageHash = sha256(message); + // using buffer.from causes error because of schnorr type checking return schnorr.sign(secretKey, Buffer.from(messageHash, 'hex')).toString('hex'); }; @@ -30,9 +31,9 @@ const sign = (secretKey, message) => { * @throws {Error} if signature verification fails */ const verify = (publicKey, message, signature) => { - const pubkeyBuf = Buffer.from(publicKey, 'hex'); - const messageBuf = Buffer.from(message, 'hex'); - const sigBuf = Buffer.from(signature, 'hex'); + const pubkeyBuf = buffer.from(publicKey, 'hex'); + const messageBuf = buffer.from(message, 'hex'); + const sigBuf = buffer.from(signature, 'hex'); try { schnorr.verify(pubkeyBuf, messageBuf, sigBuf); } catch (error) { diff --git a/src/transaction/transaction.js b/src/transaction/transaction.js index adb6b61..0e72d10 100644 --- a/src/transaction/transaction.js +++ b/src/transaction/transaction.js @@ -4,6 +4,12 @@ const buffer = require('buffer/').Buffer; const Input = require('./input'); const Output = require('./output'); const Secp256k1 = require('@enumatech/secp256k1-js'); +const utils = require('../crypto/utils'); +const assert = require('assert'); + +const PUBLIC_KEY_SIZE = 32*2; +const HASH_SIZE = 32*2; +const SIGNATURE_SIZE = 64*2; class Transaction { /** @@ -18,6 +24,38 @@ class Transaction { this.witnesses = witnesses || []; } + /** + * helper function to check if input is hexadecimal string + * @param {string} x input string + * @param {number} length optional parameter to check length of input too + * @returns {boolean} if input is hexadecimal + */ + _isHexString = (x, length) => { + if ( + (typeof x === 'string' || x instanceof String) && + /^[0-9A-Fa-f]*$/.test(x) && + (length === undefined || length === x.length) + ) { + return true; + } else { + return false; + } + }; + + /** + * helper function to check if list of properties are defined for an object + * @param {Object} object subject of test + * @param {Array(string)} properties name of properties to check + * @throws {Error} error if a required property is missing + */ + _checkProperties(object, properties) { + for (let property of properties) { + if (!(property in object)) { + throw new Error(object + " doesn't contain " + property); + } + } + } + /** * Get Hexadecimal version of transaction * @return {string} version of tx in hexadecimal format @@ -27,85 +65,77 @@ class Transaction { let ary = []; // inputs - if ((!'inputs') in this || !Array.isArray(this.inputs)) - throw new Error("object doesn't contain inputs"); + assert(Array.isArray(this.inputs), "object doesn't contain inputs"); const inbuf = buffer.alloc(8); inbuf.writeBigUInt64LE(BigInt(this.inputs.length)); ary.push(inbuf); for (let i = 0; i < this.inputs.length; i++) { - if ( - (!'tx_hash') in this.inputs[i] || - (!'index') in this.inputs[i] || - (!'witnessProgramCommitment') in this.inputs[i] || - (!'value') in this.inputs[i] - ) - throw new Error( - 'input ' + i + " doesn't contain tx_hash, index, witnessProgramCommitment, value", - ); - else { - const tx_hash = buffer.from(this.inputs[i].tx_hash, 'hex'); - const index = buffer.alloc(8); - index.writeBigUInt64LE(BigInt(this.inputs[i].index)); - const witnessProgramCommitment = buffer.from(this.inputs[i].witnessProgramCommitment, 'hex'); - const value = buffer.alloc(8); - value.writeBigUInt64LE(BigInt(this.inputs[i].value)); - - const buf = buffer.concat([tx_hash, index, witnessProgramCommitment, value]); - ary.push(buf); - } + this._checkProperties(this.inputs[i], ['tx_hash', 'index', 'witnessProgramCommitment', 'value']); + assert(this._isHexString(this.inputs[i].tx_hash, HASH_SIZE), 'invalid tx_hash'); + assert(this.inputs[i].index >= 0, 'invalid index'); + assert(this.inputs[i].value >= 0, 'invalid value'); + assert( + this._isHexString(this.inputs[i].witnessProgramCommitment, HASH_SIZE), + 'invalid witnessProgramCommitment', + ); + + const tx_hash = buffer.from(this.inputs[i].tx_hash, 'hex'); + const index = buffer.alloc(8); + index.writeBigUInt64LE(BigInt(this.inputs[i].index)); + const witnessProgramCommitment = buffer.from(this.inputs[i].witnessProgramCommitment, 'hex'); + const value = buffer.alloc(8); + value.writeBigUInt64LE(BigInt(this.inputs[i].value)); + + const buf = buffer.concat([tx_hash, index, witnessProgramCommitment, value]); + ary.push(buf); } // outputs - if ((!'outputs') in this || !Array.isArray(this.outputs)) - throw new Error("object doesn't contain outputs"); + assert(Array.isArray(this.inputs), "object doesn't contain outputs"); const outbuf = buffer.alloc(8); outbuf.writeBigUInt64LE(BigInt(this.outputs.length)); ary.push(outbuf); for (let i = 0; i < this.outputs.length; i++) { - if ((!'witnessProgramCommitment') in this.outputs[i] || (!'value') in this.outputs[i]) - throw new Error('output ' + i + " doesn't contain witnessProgramCommitment, value"); - else { - const witnessProgramCommitment = buffer.from(this.outputs[i].witnessProgramCommitment, 'hex'); - const value = buffer.alloc(8); - value.writeBigUInt64LE(BigInt(this.outputs[i].value)); - - const buf = buffer.concat([witnessProgramCommitment, value]); - ary.push(buf); - } + this._checkProperties(this.outputs[i], ['witnessProgramCommitment', 'value']); + assert( + this._isHexString(this.outputs[i].witnessProgramCommitment, HASH_SIZE), + 'invalid witnessProgramCommitment', + ); + assert(this.outputs[i].value >= 0, 'invalid value'); + + const witnessProgramCommitment = buffer.from(this.outputs[i].witnessProgramCommitment, 'hex'); + const value = buffer.alloc(8); + value.writeBigUInt64LE(BigInt(this.outputs[i].value)); + + const buf = buffer.concat([witnessProgramCommitment, value]); + ary.push(buf); } // witnesses - if ((!'witnesses') in this || !Array.isArray(this.witnesses)) - throw new Error("object doesn't contain witnesses"); + assert(Array.isArray(this.witnesses), "object doesn't contain witnesses"); const witnessbuf = buffer.alloc(8); witnessbuf.writeBigUInt64LE(BigInt(this.witnesses.length)); ary.push(witnessbuf); for (let i = 0; i < this.witnesses.length; i++) { - if ( - (!'witness_length') in this.witnesses[i] || - (!'script_type') in this.witnesses[i] || - (!'pubkey') in this.witnesses[i] || - (!'signature') in this.witnesses[i] - ) - throw new Error( - 'witness ' + i + " doesn't contain witness_length, script_type, pubkey, signature", - ); - else { - const witness_length = buffer.alloc(8); - witness_length.writeBigUInt64LE(BigInt(this.witnesses[i].witness_length)); - const script_type = buffer.from([this.witnesses[i].script_type]); - const pubkey = buffer.from(this.witnesses[i].pubkey, 'hex'); - const signature = buffer.from(this.witnesses[i].signature, 'hex'); - - const buf = buffer.concat([witness_length, script_type, pubkey, signature]); - ary.push(buf); - } + this._checkProperties(this.witnesses[i], ['witness_length', 'script_type', 'pubkey', 'signature']); + assert(this.witnesses[i].script_type >= 0 && this.witnesses[i].script_type < 64, 'invalid script_type'); + assert(this._isHexString(this.witnesses[i].pubkey, PUBLIC_KEY_SIZE), 'invalid pubkey'); + assert(this._isHexString(this.witnesses[i].signature, SIGNATURE_SIZE), 'invalid signature'); + + const witness_length = buffer.alloc(8); + witness_length.writeBigUInt64LE(BigInt(this.witnesses[i].witness_length)); + const script_type = buffer.from([this.witnesses[i].script_type]); + const pubkey = buffer.from(this.witnesses[i].pubkey, 'hex'); + const signature = buffer.from(this.witnesses[i].signature, 'hex'); + + const buf = buffer.concat([witness_length, script_type, pubkey, signature]); + ary.push(buf); } return buffer.concat(ary).toString('hex'); @@ -113,7 +143,7 @@ class Transaction { /** * Get Hexadecimal for compact version of transaction - * @param {Array(Buffer)} sentinelAttestation sentinel attestations (=public_key+signature) + * @param {Array(string)} sentinelAttestation list of sentinel attestations in hex (=public_key+signature) * @return {string} compact-tx in hexadecimal format */ getCompactHex(sentinelAttestation) { @@ -130,64 +160,66 @@ class Transaction { ary.push(inbuf); for (let i = 0; i < this.inputs.length; i++) { - if ( - (!'tx_hash') in this.inputs[i] || - (!'index') in this.inputs[i] || - (!'witnessProgramCommitment') in this.inputs[i] || - (!'value') in this.inputs[i] - ) - throw new Error( - 'input ' + i + " doesn't contain tx_hash, index, witnessProgramCommitment, value", - ); - else { - const hash = crypto.createHash('sha256'); - - const index_buf = buffer.alloc(8); - index_buf.writeBigUInt64LE(BigInt(this.inputs[i].index)); - const value_buf = buffer.alloc(8); - value_buf.writeBigUInt64LE(BigInt(this.inputs[i].value)); - const input_buf = buffer.concat([ - buffer.from(this.inputs[i].tx_hash, 'hex'), - index_buf, - buffer.from(this.inputs[i].witnessProgramCommitment, 'hex'), - value_buf, - ]); - - hash.update(input_buf); - ary.push(hash.digest()); - } + this._checkProperties(this.inputs[i], ['tx_hash', 'index', 'witnessProgramCommitment', 'value']); + assert( + this._isHexString(this.inputs[i].witnessProgramCommitment, HASH_SIZE), + 'invalid witnessProgramCommitment', + ); + assert(this.inputs[i].index >= 0, 'invalid index'); + assert(this.inputs[i].value >= 0, 'invalid value'); + assert(this._isHexString(this.inputs[i].tx_hash, HASH_SIZE), 'invalid tx_hash'); + + const hash = crypto.createHash('sha256'); + const index_buf = buffer.alloc(8); + index_buf.writeBigUInt64LE(BigInt(this.inputs[i].index)); + const value_buf = buffer.alloc(8); + value_buf.writeBigUInt64LE(BigInt(this.inputs[i].value)); + const input_buf = buffer.concat([ + buffer.from(this.inputs[i].tx_hash, 'hex'), + index_buf, + buffer.from(this.inputs[i].witnessProgramCommitment, 'hex'), + value_buf, + ]); + + hash.update(input_buf); + ary.push(hash.digest()); } // outputs - if ((!'outputs') in this || !Array.isArray(this.outputs)) - throw new Error("object doesn't contain outputs"); + assert(Array.isArray(this.outputs), "object doesn't contain outputs"); const outbuf = buffer.alloc(8); outbuf.writeBigUInt64LE(BigInt(this.outputs.length)); ary.push(outbuf); for (let i = 0; i < this.outputs.length; i++) { - if ((!'witnessProgramCommitment') in this.outputs[i] || (!'value') in this.outputs[i]) - throw new Error('output ' + i + " doesn't contain witnessProgramCommitment, value"); - else { - const hash = crypto.createHash('sha256'); - - const index_buf = buffer.alloc(8); - index_buf.writeBigUInt64LE(BigInt(i)); - const witnessProgramCommitment = buffer.from(this.outputs[i].witnessProgramCommitment, 'hex'); - const value_buf = buffer.alloc(8); - value_buf.writeBigUInt64LE(BigInt(this.outputs[i].value)); - - hash.update(buffer.concat([txidbuf, index_buf, witnessProgramCommitment, value_buf])); - ary.push(hash.digest()); - } + this._checkProperties(this.outputs[i], ['witnessProgramCommitment', 'value']); + assert( + this._isHexString(this.outputs[i].witnessProgramCommitment, HASH_SIZE), + 'invalid witnessProgramCommitment', + ); + assert(this.outputs[i].value >= 0, 'invalid value'); + + const hash = crypto.createHash('sha256'); + + const index_buf = buffer.alloc(8); + index_buf.writeBigUInt64LE(BigInt(i)); + const witnessProgramCommitment = buffer.from(this.outputs[i].witnessProgramCommitment, 'hex'); + const value_buf = buffer.alloc(8); + value_buf.writeBigUInt64LE(BigInt(this.outputs[i].value)); + + hash.update(buffer.concat([txidbuf, index_buf, witnessProgramCommitment, value_buf])); + ary.push(hash.digest()); } - const witSizebuf = buffer.alloc(8); - witSizebuf.writeBigUInt64LE(BigInt(sentinelAttestation.length)); - ary.push(witSizebuf); + // sentinel attestations + assert(Array.isArray(sentinelAttestation), 'sentinel attestation should be array'); + const sentAttestSizebuf = buffer.alloc(8); + sentAttestSizebuf.writeBigUInt64LE(BigInt(sentinelAttestation.length)); + ary.push(sentAttestSizebuf); for (let i = 0; i < sentinelAttestation.length; i++) { - ary.push(sentinelAttestation[i]); + assert(this._isHexString(sentinelAttestation[i], PUBLIC_KEY_SIZE + SIGNATURE_SIZE), 'invalid attestation'); + ary.push(buffer.from(sentinelAttestation[i], 'hex')); } return buffer.concat(ary).toString('hex'); @@ -202,54 +234,42 @@ class Transaction { let ary = []; // inputs - if (!Array.isArray(this.inputs)) throw new Error("object doesn't contain inputs"); + assert(Array.isArray(this.inputs), "object doesn't contain inputs"); const inputLen = buffer.alloc(8); inputLen.writeBigUInt64LE(BigInt(this.inputs.length)); ary.push(inputLen); for (let i = 0; i < this.inputs.length; i++) { - if ( - (!'tx_hash') in this.inputs[i] || - (!'index') in this.inputs[i] || - (!'witnessProgramCommitment') in this.inputs[i] || - (!'value') in this.inputs[i] - ) - throw new Error( - 'input ' + i + " doesn't contain tx_hash, index, witnessProgramCommitment, value", - ); - else { - const tx_hash = buffer.from(this.inputs[i].tx_hash, 'hex'); - const index = buffer.allocUnsafe(8); - index.writeBigUInt64LE(BigInt(this.inputs[i].index)); - const witnessProgramCommitment = Buffer.from(this.inputs[i].witnessProgramCommitment, 'hex'); - const value = buffer.allocUnsafe(8); - value.writeBigUInt64LE(BigInt(this.inputs[i].value)); - - const buf = buffer.concat([tx_hash, index, witnessProgramCommitment, value]); - ary.push(buf); - } + this._checkProperties(this.inputs[i], ['tx_hash', 'index', 'witnessProgramCommitment', 'value']); + + const tx_hash = buffer.from(this.inputs[i].tx_hash, 'hex'); + const index = buffer.allocUnsafe(8); + index.writeBigUInt64LE(BigInt(this.inputs[i].index)); + const witnessProgramCommitment = buffer.from(this.inputs[i].witnessProgramCommitment, 'hex'); + const value = buffer.allocUnsafe(8); + value.writeBigUInt64LE(BigInt(this.inputs[i].value)); + + const buf = buffer.concat([tx_hash, index, witnessProgramCommitment, value]); + ary.push(buf); } // outputs - if ((!'outputs') in this || !Array.isArray(this.outputs)) - throw new Error("object doesn't contain outputs"); + assert(Array.isArray(this.outputs), "object doesn't contain outputs"); const outputLen = buffer.alloc(8); outputLen.writeBigUInt64LE(BigInt(this.outputs.length)); ary.push(outputLen); for (let i = 0; i < this.outputs.length; i++) { - if ((!'witnessProgramCommitment') in this.outputs[i] || (!'value') in this.outputs[i]) - throw new Error('output ' + i + " doesn't contain witnessProgramCommitment, value"); - else { - const witnessProgramCommitment = Buffer.from(this.outputs[i].witnessProgramCommitment, 'hex'); - const value = buffer.allocUnsafe(8); - value.writeBigUInt64LE(BigInt(this.outputs[i].value)); - - const buf = buffer.concat([witnessProgramCommitment, value]); - ary.push(buf); - } + this._checkProperties(this.outputs[i], ['witnessProgramCommitment', 'value']); + + const witnessProgramCommitment = buffer.from(this.outputs[i].witnessProgramCommitment, 'hex'); + const value = buffer.allocUnsafe(8); + value.writeBigUInt64LE(BigInt(this.outputs[i].value)); + + const buf = buffer.concat([witnessProgramCommitment, value]); + ary.push(buf); } hash.update(buffer.concat(ary)); @@ -328,14 +348,14 @@ class Transaction { * @return Buffer - signed tx */ sign(secretKey) { - // TODO hexadecimal version of unsigned tx + // TODO hexadecimal version of unsigned tx (?) - let tx_hash = this.getTxid(); + const tx_hash = this.getTxid(); const pubHex = Secp256k1.generatePublicKeyFromPrivateKeyData(Secp256k1.uint256(secretKey, 16)).x; + // using buffer.from causes error because of schnorr type checking + const tx_hash_buf = Buffer.from(tx_hash, 'hex'); + const sig = schnorr.sign(secretKey, tx_hash_buf); for (let i = 0; i < this.inputs.length; i++) { - let tx_hash_buf = Buffer.from(tx_hash, 'hex'); // switch to node js buffer so schnorr module recognizes buffer; TODO must fix so one Buffer is used throughout - const sig = schnorr.sign(secretKey, tx_hash_buf); - this.witnesses.push({ witness_length: BigInt(97), script_type: 0, diff --git a/test/transaction/transaction.js b/test/transaction/transaction.js index 99ae9cb..a68a737 100644 --- a/test/transaction/transaction.js +++ b/test/transaction/transaction.js @@ -31,14 +31,24 @@ describe('Transaction', function () { }); it('test compact tx creation', function () { - const input = new Input('123456', 0, 'abcdef1234', 10000); - const output = new Output('abdcefg', 10000); - let tx = new Transaction([input], [output], []); + const input = new Input('4631418e129273ff96cc19ad256654f92c9de84fcb09e29fa3fcbd02472c2f80', 0, '195190cdaf659d1f9965a727496b67f0ba92c84277f1aa14bb1513a96b68e6e9', 50); + const output1 = new Output('195190cdaf659d1f9965a727496b67f0ba92c84277f1aa14bb1513a96b68e6e9', 20); + const output2 = new Output('33a49706ca3219e81045111121d458d369ea5079626bd71f3142acd0473c32df', 30); + const tx = new Transaction([input], [output1, output2], []); expect(tx.getCompactHex([])).equal( - '8379bb3d6b0ed2835f799d504215dc8cc2557c4f9702a3a157887c8a14a0800b010000000000000005370fa24e7bfb676535afeb606869b4fb403114f566b08154e17da89faf329a01000000000000001b11438bcb2fa942a3ddc0278455cde174a29e93c72f8e1aa486658710ba3f6e0000000000000000', + '7c2e9ce49a3f8f17b315fd9b716e806489a1418ef91b209aea576487b11f237501000000000000004ff6e1f71b0beaa0c7288101f4b36968f447d67792079736cc5393efa66d23230200000000000000449e068c4b13e3924b68b1829fe07f7c70a50421daf80f3cc4c42edde62db96ccc2c7a8da4b8c9f82070b6441ceeaa252ae61a30fcd7b128ba7bf574d5a1784d0000000000000000', ); - expect(tx.getCompactHex([Buffer.from('test')])).equal( - '8379bb3d6b0ed2835f799d504215dc8cc2557c4f9702a3a157887c8a14a0800b010000000000000005370fa24e7bfb676535afeb606869b4fb403114f566b08154e17da89faf329a01000000000000001b11438bcb2fa942a3ddc0278455cde174a29e93c72f8e1aa486658710ba3f6e010000000000000074657374', + expect(tx.getCompactHex(['eaa649f21f51bdbae7be4ae34ce6e5217a58fdce7f47f9aa7f3b58fa2120e2b3f1b1b73bd2260b86dbb24d9150e44ec8bbda366f69528b207d5e755b45684dde9f42c61a504d58f19648c30548f00a64e7131cb92411d1a893403ad04afb8949'])).equal( + '7c2e9ce49a3f8f17b315fd9b716e806489a1418ef91b209aea576487b11f237501000000000000004ff6e1f71b0beaa0c7288101f4b36968f447d67792079736cc5393efa66d23230200000000000000449e068c4b13e3924b68b1829fe07f7c70a50421daf80f3cc4c42edde62db96ccc2c7a8da4b8c9f82070b6441ceeaa252ae61a30fcd7b128ba7bf574d5a1784d0100000000000000eaa649f21f51bdbae7be4ae34ce6e5217a58fdce7f47f9aa7f3b58fa2120e2b3f1b1b73bd2260b86dbb24d9150e44ec8bbda366f69528b207d5e755b45684dde9f42c61a504d58f19648c30548f00a64e7131cb92411d1a893403ad04afb8949', ); }); + + it('check hex string', function () { + const tx = new Transaction([], [], []); + expect(tx._isHexString('g')).equal(false); + expect(tx._isHexString(0)).equal(false); + expect(tx._isHexString('0123456789abcdefABCDEF')).equal(true); + expect(tx._isHexString('0123456789abcdefABCDEF', 5)).equal(false); + expect(tx._isHexString('0123456789abcdefABCDEF', 22)).equal(true); + }); });