/*
* 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;