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