Skip to content

Commit

Permalink
feat(connector-iroha2): sending transactions signed on the client-side
Browse files Browse the repository at this point in the history
- Add new endpoint `generate-transaction`, to create unsigned transactions
  that can be signed on the client side.
- Add a function to iroha2-connector package to help signing iroha transactions
  on the client (BLP) side.
- Extend transact endpoint to accept signed transaction as an argument as well.
- Add new test suite to check features implemented in this PR (i.e. signing on the client side).

Relates to #2077

Depends on #2153

Signed-off-by: Michal Bajer <michal.bajer@fujitsu.com>
  • Loading branch information
outSH committed Oct 4, 2022
1 parent 9330dca commit 8b74234
Show file tree
Hide file tree
Showing 13 changed files with 1,111 additions and 229 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -337,9 +337,31 @@
}
}
},
"TransactRequestV1": {
"IrohaTransactionParametersV1": {
"type": "object",
"description": "Iroha V2 transaction payload parameters",
"additionalProperties": true,
"properties": {
"ttl": {
"type": "string",
"description": "BigInt time to live.",
"nullable": false
},
"creationTime": {
"type": "string",
"description": "BigInt creation time",
"nullable": false
},
"nonce": {
"type": "number",
"description": "Transaction nonce",
"nullable": false
}
}
},
"IrohaTransactionDefinitionV1": {
"type": "object",
"description": "Request to transact endpoint, can be passed one or multiple instructions to be executed.",
"description": "Iroha V2 transaction definition",
"required": [
"instruction"
],
Expand All @@ -359,6 +381,29 @@
}
]
},
"params": {
"$ref": "#/components/schemas/IrohaTransactionParametersV1",
"description": "Transaction parameters",
"nullable": false
}
}
},
"TransactRequestV1": {
"type": "object",
"description": "Request to transact endpoint.",
"additionalProperties": false,
"properties": {
"signedTransaction": {
"description": "Signed transaction binary data received from generate-transaction endpoint.",
"type": "string",
"format": "binary",
"nullable": false
},
"transaction": {
"$ref": "#/components/schemas/IrohaTransactionDefinitionV1",
"description": "New transaction definition. Caller must provide signing credential in `baseConfig`.",
"nullable": false
},
"baseConfig": {
"$ref": "#/components/schemas/Iroha2BaseConfig",
"description": "Iroha V2 connection configuration.",
Expand All @@ -379,6 +424,26 @@
}
}
},
"GenerateTransactionRequestV1": {
"type": "object",
"description": "Request for generating transaction payload that can be signed on the client side.",
"additionalProperties": false,
"required": [
"transaction"
],
"properties": {
"transaction": {
"$ref": "#/components/schemas/IrohaTransactionDefinitionV1",
"description": "New transaction definition. Caller must provide signing credential in `baseConfig`.",
"nullable": false
},
"baseConfig": {
"$ref": "#/components/schemas/Iroha2BaseConfig",
"description": "Iroha V2 connection configuration.",
"nullable": false
}
}
},
"QueryRequestV1": {
"type": "object",
"description": "Request to query endpoint.",
Expand Down Expand Up @@ -484,6 +549,51 @@
}
}
},
"/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-iroha2/generate-transaction": {
"post": {
"x-hyperledger-cactus": {
"http": {
"verbLowerCase": "post",
"path": "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-iroha2/generate-transaction"
}
},
"operationId": "generateTransactionV1",
"summary": "Generate transaction that can be signed locally.",
"parameters": [],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GenerateTransactionRequestV1"
}
}
}
},
"responses": {
"200": {
"description": "OK",
"content": {
"application/octet-stream": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
},
"500": {
"description": "Internal Server Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorExceptionResponseV1"
}
}
}
}
}
}
},
"/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-iroha2/query": {
"post": {
"x-hyperledger-cactus": {
Expand Down Expand Up @@ -529,4 +639,4 @@
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@

import { crypto } from "@iroha2/crypto-target-node";
import {
Client,
Signer,
Torii,
setCrypto,
CreateToriiProps,
makeTransactionPayload,
executableIntoSignedTransaction,
} from "@iroha2/client";
import {
AssetDefinitionId,
Expand Down Expand Up @@ -39,8 +40,14 @@ import {
NewAccount,
VecPublicKey,
TransferBox,
TransactionPayload,
AccountId,
VersionedTransaction,
} from "@iroha2/data-model";
import { Key, KeyPair } from "@iroha2/crypto-core";

// This module can't be imported unless we use `nodenext` moduleResolution
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { adapter: irohaWSAdapter } = require("@iroha2/client/web-socket/node");

import {
Expand Down Expand Up @@ -103,6 +110,22 @@ interface NamedIrohaV2Instruction {
instruction: Instruction;
}

/**
* Raw type of executableIntoSignedTransaction payloadParams parameter.
*/
type IrohaInPayloadParams = Parameters<
typeof executableIntoSignedTransaction
>[0]["payloadParams"];

/**
* Transaction parameters type to be send in payload.
* Comes from Iroha SDK.
*/
export type TransactionPayloadParameters = Exclude<
IrohaInPayloadParams,
undefined
>;

/**
* Cactus wrapper around Iroha V2 SDK Client. Should not be used outside of this connector.
* - Provides convenient functions to transact / query the ledger.
Expand All @@ -115,41 +138,59 @@ export class CactusIrohaV2Client {
private readonly transactions: Array<NamedIrohaV2Instruction> = [];

/**
* Upstream IrohaV2 SDK client used by this wrapper.
* Iroha Torii client used to send transactions to the ledger.
*/
public readonly irohaClient: Client;
public readonly irohaToriiClient: Torii;

/**
* Separate interface for making the IrohaV2 queries.
* Iroha signer used to sign transaction with user private key and account.
*/
public readonly query: CactusIrohaV2QueryClient;
public readonly irohaSigner?: Signer;

private readonly _query?: CactusIrohaV2QueryClient;

/**
* Separate interface for sending IrohaV2 queries.
* Will throw if query interface is not available
* (when client was created without a signer)
*/
public get query(): CactusIrohaV2QueryClient {
if (this._query) {
return this._query;
} else {
throw new Error("Query not available - you must provide signer key");
}
}

constructor(
public readonly toriiOptions: Omit<CreateToriiProps, "ws" | "fetch">,
public readonly signerOptions: ConstructorParameters<typeof Signer>,
public readonly accountId: AccountId,
private readonly keyPair?: KeyPair,
private readonly logLevel: LogLevelDesc = "info",
) {
Checks.truthy(toriiOptions.apiURL, "toriiOptions apiURL");
Checks.truthy(toriiOptions.telemetryURL, "toriiOptions telemetryURL");
Checks.truthy(signerOptions[0], "signerOptions account");
Checks.truthy(signerOptions[1], "signerOptions keyPair");
Checks.truthy(accountId, "signerOptions accountId");

const torii = new Torii({
this.irohaToriiClient = new Torii({
...toriiOptions,
ws: irohaWSAdapter,
fetch: undiciFetch as any,
});

const signer = new Signer(...signerOptions);

this.irohaClient = new Client({ torii, signer });

const label = this.constructor.name;
this.log = LoggerProvider.getOrCreate({ level: this.logLevel, label });

this.log.debug(`${label} created`);

this.query = new CactusIrohaV2QueryClient(this.irohaClient, this.log);
if (keyPair) {
this.log.debug("KeyPair present, add Signer and Query function.");
this.irohaSigner = new Signer(accountId, keyPair);
this._query = new CactusIrohaV2QueryClient(
this.irohaToriiClient,
this.irohaSigner,
this.log,
);
}
}

/**
Expand Down Expand Up @@ -581,31 +622,93 @@ export class CactusIrohaV2Client {
}

/**
* Send all the stored instructions as single Iroha transaction.
* Create Iroha SDK compatible `Executable` from instructions saved in current client session.
*
* @returns this
* @returns Iroha `Executable`
*/
public async send(): Promise<this> {
if (this.transactions.length === 0) {
this.log.warn("send() ignored - no instructions to be sent!");
return this;
}

private createIrohaExecutable(): Executable {
const irohaInstructions = this.transactions.map(
(entry) => entry.instruction,
);
this.log.info(
`Send transaction with ${irohaInstructions.length} instructions to Iroha ledger`,
`Created executable with ${irohaInstructions.length} instructions.`,
);

return Executable("Instructions", VecInstruction(irohaInstructions));
}

/**
* Throw if there are no instructions in current client session.
*/
private assertTransactionsNotEmpty() {
if (this.transactions.length === 0) {
throw new Error(
"assertTransactionsNotEmpty() failed - no instructions defined!",
);
}
}

/**
* Get transaction payload buffer that can be signed and then sent to the ledger.
*
* @param txParams Transaction parameters.
*
* @returns Buffer of encoded `TransactionPayload`
*/
public getTransactionPayloadBuffer(
txParams?: TransactionPayloadParameters,
): Uint8Array {
this.assertTransactionsNotEmpty();

const payload = makeTransactionPayload({
accountId: this.accountId,
executable: this.createIrohaExecutable(),
...txParams,
});

return TransactionPayload.toBuffer(payload);
}

/**
* Send all the stored instructions as single Iroha transaction.
*
* @param txParams Transaction parameters.
*
* @returns void
*/
public async send(txParams?: TransactionPayloadParameters): Promise<void> {
this.assertTransactionsNotEmpty();
if (!this.irohaSigner) {
throw new Error("send() failed - no Iroha Signer, keyPair was missing");
}

this.log.debug(this.getTransactionSummary());

await this.irohaClient.submitExecutable(
Executable("Instructions", VecInstruction(irohaInstructions)),
);
const signedTx = executableIntoSignedTransaction({
signer: this.irohaSigner,
executable: this.createIrohaExecutable(),
payloadParams: txParams,
});

await this.sendSignedPayload(signedTx);
this.clear();
}

return this;
/**
* Send signed transaction payload to the ledger.
*
* @param signedPayload encoded or plain `VersionedTransaction`
*/
public async sendSignedPayload(
signedPayload: VersionedTransaction | ArrayBufferView,
): Promise<void> {
Checks.truthy(signedPayload, "sendSignedPayload arg signedPayload");

if (ArrayBuffer.isView(signedPayload)) {
signedPayload = VersionedTransaction.fromBuffer(signedPayload);
}

await this.irohaToriiClient.submit(signedPayload);
}

/**
Expand All @@ -614,8 +717,7 @@ export class CactusIrohaV2Client {
*/
public free(): void {
this.log.debug("Free CactusIrohaV2Client key pair");
// TODO - Investigate if signer keypair not leaking now
//this.irohaClient.keyPair?.free();
this.keyPair?.free();
this.clear();
}
}
Loading

0 comments on commit 8b74234

Please sign in to comment.