diff --git a/README.md b/README.md index ba36703..13e43d2 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([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 + 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..87ae2b0 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 {string} witness commitment in hex + */ + getWitnessCommit(scriptType = '00') { + return utils.sha256(buffer.from(scriptType + this.publicKey, '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/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 064c942..090af50 100644 --- a/src/crypto/utils.js +++ b/src/crypto/utils.js @@ -1,13 +1,15 @@ const schnorr = require('bip-schnorr'); +const crypto = require('crypto'); +const buffer = require('buffer/').Buffer; /** - * Return sha256 of input b - * @param {*} b - * @returns + * Return sha256 of input + * @param {*} msg + * @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} @@ -17,6 +19,7 @@ const sha256 = function a(b){function c(a,b){return a>>>b|a<<32-b;}for(var d,e,f */ 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'); }; @@ -28,13 +31,13 @@ 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) { - console.log('Error msg: ', error); + console.log('Error msg: ', error); return false; } return true; @@ -43,5 +46,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 18c808f..4b5d2ec 100644 --- a/src/networking/broadcast.js +++ b/src/networking/broadcast.js @@ -2,39 +2,58 @@ 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 - broadcastTx: (port, host, signedTxHex) => { - - 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 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); + /** + * 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 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]); + + 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); }); }); - - client.on('data', (data) => { - console.log('Received: ' + data.toString('hex')); - client.destroy(); // kill client after server's response - }); - - client.on('close', () => { - console.log('Connection closed'); - }); - - client.on('error', (err) => { - console.error(err); - }); }, }; 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..0e72d10 100644 --- a/src/transaction/transaction.js +++ b/src/transaction/transaction.js @@ -4,10 +4,16 @@ 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 { /** - * + * * @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) @@ -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,87 +65,168 @@ 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= 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); + const outbuf = buffer.alloc(8); outbuf.writeBigUInt64LE(BigInt(this.outputs.length)); ary.push(outbuf); - for (let i=0; i= 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= 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'); } /** - * + * Get Hexadecimal for compact version of transaction + * @param {Array(string)} sentinelAttestation list of sentinel attestations in hex (=public_key+signature) + * @return {string} compact-tx in hexadecimal format + */ + getCompactHex(sentinelAttestation) { + // Buffer concat necessary information to bytes + let ary = []; + + // tx id + const txidbuf = buffer.from(this.getTxid(), 'hex'); + ary.push(txidbuf); + + // there can be zero inputs (for mint tx) + const inbuf = buffer.alloc(8); + inbuf.writeBigUInt64LE(BigInt(this.inputs.length)); + ary.push(inbuf); + + for (let i = 0; i < this.inputs.length; i++) { + 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 + 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++) { + 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()); + } + + // 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++) { + 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'); + } + + /** + * * @returns {string} hexadecimal txid for the current transaction */ getTxid() { @@ -115,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