Skip to content

Commit

Permalink
Added support for Besu private transactions
Browse files Browse the repository at this point in the history
Signed-off-by: Lucas Saldanha <lucascrsaldanha@gmail.com>
  • Loading branch information
lucassaldanha committed Oct 2, 2020
1 parent 5938426 commit c3a5701
Show file tree
Hide file tree
Showing 12 changed files with 426 additions and 25 deletions.
6 changes: 3 additions & 3 deletions packages/caliper-cli/lib/lib/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@ sut:

besu:
1.3.2:
packages: ['web3@1.2.2']
packages: ['web3@1.3.0']
1.3:
packages: ['web3@1.2.2']
packages: ['web3@1.3.0']
1.4: &besu-latest
packages: ['web3@1.2.2']
packages: ['web3@1.3.0']
latest: *besu-latest
219 changes: 200 additions & 19 deletions packages/caliper-ethereum/lib/ethereum-connector.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

const EthereumHDKey = require('ethereumjs-wallet/hdkey');
const Web3 = require('web3');
const EEAClient = require('web3-eea');
const {ConnectorBase, CaliperUtils, ConfigUtil, TxStatus} = require('@hyperledger/caliper-core');

const logger = CaliperUtils.getLogger('ethereum-connector');
Expand Down Expand Up @@ -50,6 +51,9 @@ class EthereumConnector extends ConnectorBase {

this.ethereumConfig = ethereumConfig;
this.web3 = new Web3(this.ethereumConfig.url);
if (this.ethereumConfig.privacy) {
this.web3eea = new EEAClient(this.web3, ethereumConfig.chainId);
}
this.web3.transactionConfirmationBlocks = this.ethereumConfig.transactionConfirmationBlocks;
this.workerIndex = workerIndex;
this.context = undefined;
Expand Down Expand Up @@ -100,13 +104,29 @@ class EthereumConnector extends ConnectorBase {
let self = this;
logger.info('Creating contracts...');
for (const key of Object.keys(this.ethereumConfig.contracts)) {
let contractData = require(CaliperUtils.resolvePath(this.ethereumConfig.contracts[key].path)); // TODO remove path property
let contractGas = this.ethereumConfig.contracts[key].gas;
let estimateGas = this.ethereumConfig.contracts[key].estimateGas;
const contract = this.ethereumConfig.contracts[key];
const contractData = require(CaliperUtils.resolvePath(contract.path)); // TODO remove path property
const contractGas = contract.gas;
const estimateGas = contract.estimateGas;
let privacy;
if (this.ethereumConfig.privacy) {
privacy = this.ethereumConfig.privacy[contract.private];
}

this.ethereumConfig.contracts[key].abi = contractData.abi;
promises.push(new Promise(async function(resolve, reject) {
let contractInstance = await self.deployContract(contractData);
logger.info('Deployed contract ' + contractData.name + ' at ' + contractInstance.options.address);
let contractInstance;
try {
if (privacy) {
contractInstance = await self.deployPrivateContract(contractData, privacy);
logger.info('Deployed private contract ' + contractData.name + ' at ' + contractInstance.options.address);
} else {
contractInstance = await self.deployContract(contractData);
logger.info('Deployed contract ' + contractData.name + ' at ' + contractInstance.options.address);
}
} catch (err) {
reject(err);
}
self.ethereumConfig.contracts[key].address = contractInstance.options.address;
self.ethereumConfig.contracts[key].gas = contractGas;
self.ethereumConfig.contracts[key].estimateGas = estimateGas;
Expand Down Expand Up @@ -171,6 +191,11 @@ class EthereumConnector extends ConnectorBase {
await context.web3.eth.personal.unlockAccount(this.ethereumConfig.fromAddress, this.ethereumConfig.fromAddressPassword, 1000);
}

if (this.ethereumConfig.privacy) {
context.web3eea = this.web3eea;
context.privacy = this.ethereumConfig.privacy;
}

this.context = context;
return context;
}
Expand All @@ -189,6 +214,10 @@ class EthereumConnector extends ConnectorBase {
* @return {Promise<TxStatus>} Result and stats of the transaction invocation.
*/
async _sendSingleRequest(request) {
if (request.privacy) {
return this._sendSinglePrivateRequest(request);
}

const context = this.context;
let status = new TxStatus();
let params = {from: context.fromAddress};
Expand Down Expand Up @@ -254,28 +283,129 @@ class EthereumConnector extends ConnectorBase {
return status;
}

/**
* Submit a private transaction to the ethereum context.
* @param {EthereumInvoke} request Methods call data.
* @return {Promise<TxStatus>} Result and stats of the transaction invocation.
*/
async _sendSinglePrivateRequest(request) {
const context = this.context;
const web3eea = context.web3eea;
const contractInfo = context.contracts[request.contract];
const privacy = request.privacy;
const sender = privacy.sender;

const status = new TxStatus();

const onFailure = (err) => {
status.SetStatusFail();
logger.error('Failed private tx on ' + request.contract + ' calling method ' + request.verb + ' private nonce ' + 0);
logger.error(err);
};

const onSuccess = (rec) => {
status.SetID(rec.transactionHash);
status.SetResult(rec);
status.SetVerification(true);
status.SetStatusSuccess();
};

let payload;
if (request.args) {
payload = contractInfo.contract.methods[request.verb](...request.args).encodeABI();
} else {
payload = contractInfo.contract.methods[request.verb]().encodeABI();
}

const transaction = {
to: contractInfo.contract._address,
data: payload
};

try {
if (request.readOnly) {
transaction.privacyGroupId = await this.resolvePrivacyGroup(privacy);

const value = await web3eea.priv.call(transaction);
onSuccess(value);
} else {
transaction.nonce = sender.nonce;
transaction.privateKey = sender.privateKey.substring(2);
this.setPrivateTransactionParticipants(transaction, privacy);

const txHash = await web3eea.eea.sendRawTransaction(transaction);
const rcpt = await web3eea.priv.getTransactionReceipt(txHash, transaction.privateFrom);
if (rcpt.status === '0x1') {
onSuccess(rcpt);
} else {
onFailure(rcpt);
}
}
} catch(err) {
onFailure(err);
}

return status;
}


/**
* Deploys a new contract using the given web3 instance
* @param {JSON} contractData Contract data with abi, bytecode and gas properties
* @returns {Promise<web3.eth.Contract>} The deployed contract instance
*/
deployContract(contractData) {
let web3 = this.web3;
let contractDeployerAddress = this.ethereumConfig.contractDeployerAddress;
return new Promise(function(resolve, reject) {
let contract = new web3.eth.Contract(contractData.abi);
let contractDeploy = contract.deploy({
data: contractData.bytecode
});
contractDeploy.send({
async deployContract(contractData) {
const web3 = this.web3;
const contractDeployerAddress = this.ethereumConfig.contractDeployerAddress;
const contract = new web3.eth.Contract(contractData.abi);
const contractDeploy = contract.deploy({
data: contractData.bytecode
});

try {
return contractDeploy.send({
from: contractDeployerAddress,
gas: contractData.gas
}).on('error', (error) => {
reject(error);
}).then((newContractInstance) => {
resolve(newContractInstance);
});
});
} catch (err) {
throw(err);
}
}

/**
* Deploys a new contract using the given web3 instance
* @param {JSON} contractData Contract data with abi, bytecode and gas properties
* @param {JSON} privacy Privacy options
* @returns {Promise<web3.eth.Contract>} The deployed contract instance
*/
async deployPrivateContract(contractData, privacy) {
const web3 = this.web3;
const web3eea = this.web3eea;
// Using randomly generated account to deploy private contract to avoid public/private nonce issues
const deployerAccount = web3.eth.accounts.create();

const transaction = {
data: contractData.bytecode,
nonce: deployerAccount.nonce,
privateKey: deployerAccount.privateKey.substring(2), // web3js-eea doesn't not accept private keys prefixed by '0x'
};

this.setPrivateTransactionParticipants(transaction, privacy);

try {
const txHash = await web3eea.eea.sendRawTransaction(transaction);
const txRcpt = await web3eea.priv.getTransactionReceipt(txHash, transaction.privateFrom);

if (txRcpt.status === '0x1') {
return new web3.eth.Contract(contractData.abi, txRcpt.contractAddress);
} else {
logger.error('Failed private transaction hash ' + txHash);
throw new Error('Failed private transaction hash ' + txHash);
}
} catch (err) {
logger.error('Error deploying private contract: ', JSON.stringify(err));
throw(err);
}
}

/**
Expand All @@ -291,6 +421,57 @@ class EthereumConnector extends ConnectorBase {
}
return result;
}

/**
* Returns the privacy group id depending on the privacy mode being used
* @param {JSON} privacy Privacy options
* @returns {Promise<string>} The privacyGroupId
*/
async resolvePrivacyGroup(privacy) {
const web3eea = this.context.web3eea;

switch(privacy.groupType) {
case 'legacy': {
const privGroups = await web3eea.priv.findPrivacyGroup({addresses: [privacy.privateFrom, ...privacy.privateFor]});
if (privGroups.length > 0) {
return privGroups.filter(function(el) {
return el.type === 'LEGACY';
})[0].privacyGroupId;
} else {
throw new Error('Multiple legacy privacy groups with same members. Can\'t resolve privacyGroupId.');
}
}
case 'pantheon':
case 'onchain': {
return privacy.privacyGroupId;
} default: {
throw new Error('Invalid privacy type');
}
}
}

/**
* Set the participants of a privacy transaction depending on the privacy mode being used
* @param {JSON} transaction Object representing the transaction fields
* @param {JSON} privacy Privacy options
*/
setPrivateTransactionParticipants(transaction, privacy) {
switch(privacy.groupType) {
case 'legacy': {
transaction.privateFrom = privacy.privateFrom;
transaction.privateFor = privacy.privateFor;
break;
}
case 'pantheon':
case 'onchain': {
transaction.privateFrom = privacy.privateFrom;
transaction.privacyGroupId = privacy.privacyGroupId;
break;
} default: {
throw new Error('Invalid privacy type');
}
}
}
}

module.exports = EthereumConnector;
5 changes: 3 additions & 2 deletions packages/caliper-ethereum/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@
},
"dependencies": {
"@hyperledger/caliper-core": "0.4.0-unstable",
"ethereumjs-wallet": "^0.6.3"
"ethereumjs-wallet": "^0.6.3",
"web3": "1.3.0",
"web3-eea": "0.10.0"
},
"devDependencies": {
"web3": "1.2.2",
"eslint": "^5.16.0",
"mocha": "3.4.2",
"nyc": "11.1.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ services:
volumes:
- ./keys:/root/.ethereum/keystore
- ./data:/root
- ./orion:/orion
ports:
- 8545-8547:8545-8547
command: --revert-reason-enabled --rpc-ws-enabled --rpc-ws-host 0.0.0.0 --host-whitelist=* --rpc-ws-apis admin,eth,miner,web3,net --graphql-http-enabled --discovery-enabled=false
command: --min-gas-price 0 --revert-reason-enabled --rpc-ws-enabled --rpc-ws-host 0.0.0.0 --host-whitelist=* --rpc-ws-apis admin,eth,miner,web3,net,priv,eea --graphql-http-enabled --discovery-enabled=false --privacy-enabled=true --privacy-url=http://orion:8888 --privacy-public-key-file=/orion/key/orion.pub
orion:
image: "pegasyseng/orion:1.6.0"
container_name: orion
command: ["/config/orion.conf"]
volumes:
- ./orion/orion.conf:/config/orion.conf
- ./orion/key/:/keys/
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"data":{"bytes":"uTJGpd4ZEEtDPFSZM0+GT11xn5NFIr2KGP2Q4SdVPRM="},"type":"unlocked"}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
GGilEkXLaQ9yhhtbpBT03Me9iYa7U/mWXxrJhnbl1XY=
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
nodeurl = "http://127.0.0.1:8080/"
nodeport = 8080
nodenetworkinterface = "0.0.0.0"

clienturl = "http://127.0.0.1:8888/"
clientport = 8888
clientnetworkinterface = "0.0.0.0"

tls = "off"

publickeys = ["/keys/orion.pub"]
privatekeys = ["/keys/orion.key"]
11 changes: 11 additions & 0 deletions packages/caliper-tests-integration/besu_tests/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "besu_tests",
"version": "1.0.0",
"description": "Caliper Besu Integration Tests",
"keywords": [],
"author": "",
"license": "Apache-2.0",
"dependencies": {
"web3": "1.3.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
test:
name: Simple Storage
description: Test private transactions
workers:
type: local
number: 1
rounds:
- label: public store
txNumber: 5
rateControl:
type: fixed-rate
opts:
tps: 100
workload:
module: ./../store.js
arguments:
contract: public_storage
- label: private store
txNumber: 5
rateControl:
type: fixed-rate
opts:
tps: 100
workload:
module: ./../store.js
arguments:
contract: private_storage
private: group1

Loading

0 comments on commit c3a5701

Please sign in to comment.