UNPKG

web3-eea

Version:
379 lines (347 loc) 10.9 kB
/* * Copyright ConsenSys Software Inc. * * This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. * If a copy of the MPL was not distributed with this file, You can obtain one at * * http://mozilla.org/MPL/2.0/ * * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. * * SPDX-License-Identifier: MPL-2.0 */ /* eslint-disable no-underscore-dangle */ const ethUtil = require("./custom-ethjs-util"); const { BN } = ethUtil; // secp256k1n/2 const N_DIV_2 = new BN( "7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0", 16 ); /** * Creates a new private transaction object. * * @example * var rawTx = { * nonce: '0x00', * gasPrice: '0x09184e72a000', * gasLimit: '0x2710', * to: '0x0000000000000000000000000000000000000000', * value: '0x00', * data: '0x7f7465737432000000000000000000000000000000000000000000000000000000600057', * v: '0x1c', * r: '0x5e1d3a76fbf824220eafc8c79ad578ad2b67d01b0c2425eb1f1347e8f50882ab', * s: '0x5bd428537f05f9830e93792f90ea6a3e2d1ee84952dd96edbae9f658f831ab13' * privateFrom: 'A1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo=' * privateFor: ['Ko2bVqD+nNlNYL5EE7y3IdOnviftjiizpjRt+HTuFBs='] * restriction: 'restricted' * }; * var tx = new PrivateTransaction(rawTx); * * @class * @param {Buffer | Array | Object} data a private transaction can be initiailized with either a buffer containing the RLP serialized private transaction or an array of buffers relating to each of the tx Properties, listed in order below in the example. * * Or lastly an Object containing the Properties of the private transaction like in the Usage example. * * For Object and Arrays each of the elements can either be a Buffer, a hex-prefixed (0x) String , Number, or an object with a toBuffer method such as Bignum * * @property {Buffer} raw The raw rlp encoded private transaction * @param {Buffer} data.nonce nonce number * @param {Buffer} data.gasLimit transaction gas limit * @param {Buffer} data.gasPrice transaction gas price * @param {Buffer} data.to to the to address * @param {Buffer} data.value the amount of ether sent * @param {Buffer} data.data this will contain the data of the message or the init of a contract * @param {Buffer} data.v EC recovery ID * @param {Buffer} data.r EC signature parameter * @param {Buffer} data.s EC signature parameter * @param {Buffer} data.privateFrom The enclave public key of the sender * @param {Array<Buffer>} data.privateFor The enclave public keys of the receivers * @param {Buffer} data.privacyGroupId The enclave id representing the group of receivers * @param {Buffer} data.restriction The transaction type - "restricted" or "unrestricted" * @param {Number} data.chainId EIP 155 chainId - mainnet: 1, ropsten: * */ class PrivateTransaction { constructor(d) { const data = d || {}; // Define Properties const fields = [ { name: "nonce", length: 32, allowLess: true, default: Buffer.from([]) }, { name: "gasPrice", length: 32, allowLess: true, default: Buffer.from([]) }, { name: "gasLimit", alias: "gas", length: 32, allowLess: true, default: Buffer.from([]) }, { name: "to", allowZero: true, length: 20, default: Buffer.from([]) }, { name: "value", length: 32, allowLess: true, default: Buffer.from([]) }, { name: "data", alias: "input", allowZero: true, default: Buffer.from([]) }, { name: "v", allowZero: true, default: Buffer.from([0x1c]) }, { name: "r", length: 32, allowZero: true, allowLess: true, default: Buffer.from([]) }, { name: "s", length: 32, allowZero: true, allowLess: true, default: Buffer.from([]) }, { name: "privateFrom", // length: 88, //apparently the length is 0 in the test... default: Buffer.from([]) }, { name: "privateFor", nullable: true, allowZero: true, // if you comment out this line test fails (for now) bufferArray: true, default: [Buffer.from([])] }, { name: "privacyGroupId", nullable: true, default: Buffer.from([]) }, { name: "restriction", default: Buffer.from([]) } ]; /** * Returns the rlp encoding of the private transaction * @method serialize * @return {Buffer} * @memberof PrivateTransaction * @name serialize * @see {@link https://github.com/ethereumjs/ethereumjs-util/blob/master/docs/index.md#defineproperties|ethereumjs-util} */ /** * Returns the private transaction in JSON format * @method toJSON * @return {Array | String} * @memberof PrivateTransaction * @name toJSON * @see {@link https://github.com/ethereumjs/ethereumjs-util/blob/master/docs/index.md#defineproperties|ethereumjs-util} */ // attached serialize ethUtil.defineProperties(this, fields, data); /** * @property {Buffer} from (read only) sender address of this private transaction, mathematically derived from other parameters. * @name from * @memberof PrivateTransaction */ Object.defineProperty(this, "from", { enumerable: true, configurable: true, get: this.getSenderAddress.bind(this) }); // calculate chainId from signature const sigV = ethUtil.bufferToInt(this.v); let chainId = Math.floor((sigV - 35) / 2); if (chainId < 0) chainId = 0; // set chainId this._chainId = chainId || data.chainId || 0; } /** * If the tx's `to` is to the creation address * @return {Boolean} */ toCreationAddress() { return this.to.toString("hex") === ""; } /** * Computes a sha3-256 hash of the serialized tx * @param {Boolean} [includeSignature=true] whether or not to inculde the signature * @return {Buffer} */ hash(includeSignature) { // eslint-disable-next-line no-param-reassign if (includeSignature === undefined) includeSignature = true; // EIP155 spec: // when computing the hash of a transaction for purposes of signing or recovering, // instead of hashing only the first six elements (ie. nonce, gasprice, startgas, to, value, data), // hash nine elements, with v replaced by CHAIN_ID, r = 0 and s = 0 let items; if (includeSignature) { items = this.raw; } else if (this._chainId > 0) { const raw = this.raw.slice(); this.v = this._chainId; this.r = 0; this.s = 0; items = this.raw; this.raw = raw; } else { items = this.raw.slice(0, 6); } const arr = items.slice(); if (items[10][0].length !== 0 && items[11].length === 32) { throw Error( "privacyGroupId and privateFor fields are mutually exclusive" ); } if (items[11].length === 32) { arr.splice(10, 1); } else { arr.splice(11, 1); } // create hash return ethUtil.rlphash(arr); } /** * returns chain ID * @return {Buffer} */ getChainId() { return this._chainId; } /** * returns the sender's address * @return {Buffer} */ getSenderAddress() { if (this._from) { return this._from; } const pubkey = this.getSenderPublicKey(); this._from = ethUtil.publicToAddress(pubkey); return this._from; } /** * returns the public key of the sender * @return {Buffer} */ getSenderPublicKey() { if (!this._senderPubKey || !this._senderPubKey.length) { if (!this.verifySignature()) throw new Error("Invalid Signature"); } return this._senderPubKey; } /** * Determines if the signature is valid * @return {Boolean} */ verifySignature() { const msgHash = this.hash(false); // All transaction signatures whose s-value is greater than secp256k1n/2 are considered invalid. if (new BN(this.s).cmp(N_DIV_2) === 1) { return false; } try { let v = ethUtil.bufferToInt(this.v); if (this._chainId > 0) { v -= this._chainId * 2 + 8; } this._senderPubKey = ethUtil.ecrecover(msgHash, v, this.r, this.s); } catch (e) { return false; } return !!this._senderPubKey; } /** * sign a private transaction with a given private key * @param {Buffer} privateKey Must be 32 bytes in length */ sign(privateKey) { const msgHash = this.hash(false); const sig = ethUtil.ecsign(msgHash, privateKey); if (this._chainId > 0) { sig.v += this._chainId * 2 + 8; } Object.assign(this, sig); } /** * The amount of gas paid for the data in this tx * @return {BN} */ getDataFee() { const data = this.raw[5]; const cost = new BN(0); // eslint-disable-next-line no-plusplus for (let i = 0; i < data.length; i++) { // eslint-disable-next-line no-unused-expressions data[i] === 0 ? cost.iaddn(this._common.param("gasPrices", "txDataZero")) : cost.iaddn(this._common.param("gasPrices", "txDataNonZero")); } return cost; } /** * the minimum amount of gas the tx must have (DataFee + TxFee + Creation Fee) * @return {BN} */ getBaseFee() { const fee = this.getDataFee().iaddn(this._common.param("gasPrices", "tx")); if (this._common.gteHardfork("homestead") && this.toCreationAddress()) { fee.iaddn(this._common.param("gasPrices", "txCreation")); } return fee; } /** * the up front amount that an account must have for this private transaction to be valid * @return {BN} */ getUpfrontCost() { return new BN(this.gasLimit) .imul(new BN(this.gasPrice)) .iadd(new BN(this.value)); } /** * validates the signature and checks to see if it has enough gas * @param {Boolean} [stringError=false] whether to return a string with a description of why the validation failed or return a Boolean * @return {Boolean|String} */ validate(stringError) { const errors = []; if (!this.verifySignature()) { errors.push("Invalid Signature"); } if (this.getBaseFee().cmp(new BN(this.gasLimit)) > 0) { errors.push([`gas limit is too low. Need at least ${this.getBaseFee()}`]); } if (stringError === undefined || stringError === false) { return errors.length === 0; } return errors.join(" "); } } module.exports = PrivateTransaction;