Skip to content

Commit

Permalink
[FAB-15615] Add ChaincodeException support
Browse files Browse the repository at this point in the history
These changes allow chaincode to send extended failure information
to client applications by including a response payload in a new ChaincodeException.

The payload could include a simple error code, or more
complex error object depending on requirements.

Also includes related error handling changes to prevent potentially
sensitive stack traces being sent to client applications.

Change-Id: Ib87efdb11abf0330e3ee87ecd988408f94b51c8d
Signed-off-by: James Taylor <[email protected]>
  • Loading branch information
jt-nti committed Jul 1, 2019
1 parent 9077581 commit 25c5be6
Show file tree
Hide file tree
Showing 12 changed files with 374 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.hyperledger.fabric.contract.annotation.Contract;
import org.hyperledger.fabric.contract.annotation.Transaction;
import org.hyperledger.fabric.shim.ChaincodeStub;
import org.hyperledger.fabric.shim.ChaincodeException;

/**
* All Contracts should implement this interface, in addition to the
Expand Down Expand Up @@ -72,7 +73,7 @@ default Context createContext(ChaincodeStub stub) {
* @param ctx the context as created by {@link #createContext(ChaincodeStub)}.
*/
default void unknownTransaction(Context ctx) {
throw new IllegalStateException("Undefined contract method called");
throw new ChaincodeException("Undefined contract method called");
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,25 +75,32 @@ void startRouting() {
}
}

@Override
public Response invoke(ChaincodeStub stub) {
private Response processRequest(ChaincodeStub stub) {
logger.info(() -> "Got invoke routing request");
if (stub.getStringArgs().size() > 0) {
logger.info(() -> "Got the invoke request for:" + stub.getFunction() + " " + stub.getParameters());
InvocationRequest request = ExecutionFactory.getInstance().createRequest(stub);
TxFunction txFn = getRouting(request);

logger.info(() -> "Got routing:" + txFn.getRouting());
return executor.executeRequest(txFn, request, stub);
} else {
return ResponseUtils.newSuccessResponse();
try {
if (stub.getStringArgs().size() > 0) {
logger.info(() -> "Got the invoke request for:" + stub.getFunction() + " " + stub.getParameters());
InvocationRequest request = ExecutionFactory.getInstance().createRequest(stub);
TxFunction txFn = getRouting(request);

logger.info(() -> "Got routing:" + txFn.getRouting());
return executor.executeRequest(txFn, request, stub);
} else {
return ResponseUtils.newSuccessResponse();
}
} catch (Throwable throwable) {
return ResponseUtils.newErrorResponse(throwable);
}
}

@Override
public Response invoke(ChaincodeStub stub) {
return processRequest(stub);
}

@Override
public Response init(ChaincodeStub stub) {
return invoke(stub);
return processRequest(stub);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
*/
package org.hyperledger.fabric.contract;

import org.hyperledger.fabric.shim.ChaincodeException;

/**
* Specific RuntimeException for events that occur in the calling and handling
* of the Contracts, NOT within the contract logic itself.
Expand All @@ -13,7 +15,7 @@
* for example current tx id
*
*/
public class ContractRuntimeException extends RuntimeException {
public class ContractRuntimeException extends ChaincodeException {

public ContractRuntimeException(String string) {
super(string);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.hyperledger.fabric.Logger;
import org.hyperledger.fabric.contract.Context;
import org.hyperledger.fabric.contract.ContractInterface;
import org.hyperledger.fabric.contract.ContractRuntimeException;
import org.hyperledger.fabric.contract.execution.ExecutionService;
import org.hyperledger.fabric.contract.execution.InvocationRequest;
import org.hyperledger.fabric.contract.execution.JSONTransactionSerializer;
Expand All @@ -23,6 +24,7 @@
import org.hyperledger.fabric.contract.routing.TxFunction;
import org.hyperledger.fabric.contract.routing.TypeRegistry;
import org.hyperledger.fabric.shim.Chaincode;
import org.hyperledger.fabric.shim.ChaincodeException;
import org.hyperledger.fabric.shim.ChaincodeStub;
import org.hyperledger.fabric.shim.ResponseUtils;

Expand Down Expand Up @@ -62,11 +64,16 @@ public Chaincode.Response executeRequest(TxFunction txFn, InvocationRequest req,
}

} catch (IllegalAccessException | InstantiationException e) {
logger.error(() -> "Error during contract method invocation" + e);
response = ResponseUtils.newErrorResponse(e);
String message = String.format("Could not execute contract method: %s", rd.toString());
throw new ContractRuntimeException(message, e);
} catch (InvocationTargetException e) {
logger.error(() -> "Error during contract method invocation" + e);
response = ResponseUtils.newErrorResponse(e.getCause());
Throwable cause = e.getCause();

if (cause instanceof ChaincodeException) {
throw (ChaincodeException) cause;
} else {
throw new ContractRuntimeException("Error during contract method execution", cause);
}
}

return response;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
package org.hyperledger.fabric.shim;

import static java.nio.charset.StandardCharsets.UTF_8;

/**
* Contracts should use {@code ChaincodeException} to indicate when an error
* occurs in Smart Contract logic.
*
* <p>
* When a {@code ChaincodeException} is thrown an error response will be
* returned from the chaincode container containing the exception message and
* payload, if specified.
*
* <p>
* {@code ChaincodeException} may be extended to provide application specific
* error information. Subclasses should ensure that {@link #getPayload} returns
* a serialized representation of the error in a suitable format for client
* applications to process.
*/
public class ChaincodeException extends RuntimeException {

private static final long serialVersionUID = 3664437023130016393L;

private byte[] payload;

/**
* Constructs a new {@code ChaincodeException} with no detail message.
*/
public ChaincodeException() {
super();
}

/**
* Constructs a new {@code ChaincodeException} with the specified detail
* message.
*
* @param message the detail message.
*/
public ChaincodeException(String message) {
super(message);
}

/**
* Constructs a new {@code ChaincodeException} with the specified cause.
*
* @param cause the cause.
*/
public ChaincodeException(Throwable cause) {
super(cause);
}

/**
* Constructs a new {@code ChaincodeException} with the specified detail
* message and cause.
*
* @param message the detail message.
* @param cause the cause.
*/
public ChaincodeException(String message, Throwable cause) {
super(message, cause);
}

/**
* Constructs a new {@code ChaincodeException} with the specified detail
* message and response payload.
*
* @param message the detail message.
* @param payload the response payload.
*/
public ChaincodeException(String message, byte[] payload) {
super(message);

this.payload = payload;
}

/**
* Constructs a new {@code ChaincodeException} with the specified detail
* message, response payload and cause.
*
* @param message the detail message.
* @param payload the response payload.
* @param cause the cause.
*/
public ChaincodeException(String message, byte[] payload, Throwable cause) {
super(message, cause);

this.payload = payload;
}

/**
* Constructs a new {@code ChaincodeException} with the specified detail
* message and response payload.
*
* @param message the detail message.
* @param payload the response payload.
*/
public ChaincodeException(String message, String payload) {
super(message);

this.payload = payload.getBytes(UTF_8);
}

/**
* Constructs a new {@code ChaincodeException} with the specified detail
* message, response payload and cause.
*
* @param message the detail message.
* @param payload the response payload.
* @param cause the cause.
*/
public ChaincodeException(String message, String payload, Throwable cause) {
super(message, cause);

this.payload = payload.getBytes(UTF_8);
}

/**
* Returns the response payload or {@code null} if there is no response.
*
* <p>
* The payload should represent the chaincode error in a way that client
* applications written in different programming languages can interpret. For
* example it could include a domain specific error code, in addition to any
* state information which would allow client applications to respond
* appropriately.
*
* @return the response payload or {@code null} if there is no response.
*/
public byte[] getPayload() {
return payload;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
*/
package org.hyperledger.fabric.shim;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;

import static org.hyperledger.fabric.shim.Chaincode.Response.Status.INTERNAL_SERVER_ERROR;
import static org.hyperledger.fabric.shim.Chaincode.Response.Status.SUCCESS;

import org.hyperledger.fabric.Logger;

public class ResponseUtils {

private static Logger logger = Logger.getLogger(ResponseUtils.class.getName());

public static Chaincode.Response newSuccessResponse(String message, byte[] payload) {
return new Chaincode.Response(SUCCESS, message, payload);
}
Expand Down Expand Up @@ -46,14 +47,18 @@ public static Chaincode.Response newErrorResponse(byte[] payload) {
}

public static Chaincode.Response newErrorResponse(Throwable throwable) {
return newErrorResponse(throwable.getMessage()==null?"":throwable.getMessage(), printStackTrace(throwable));
}
// Responses should not include internals like stack trace but make sure it gets logged
logger.error(() -> logger.formatError(throwable));

private static byte[] printStackTrace(Throwable throwable) {
if (throwable == null) return null;
final StringWriter buffer = new StringWriter();
throwable.printStackTrace(new PrintWriter(buffer));
return buffer.toString().getBytes(StandardCharsets.UTF_8);
}
String message = null;
byte[] payload = null;
if (throwable instanceof ChaincodeException) {
message = throwable.getMessage();
payload = ((ChaincodeException) throwable).getPayload();
} else {
message = "Unexpected error";
}

return ResponseUtils.newErrorResponse(message, payload);
}
}
13 changes: 11 additions & 2 deletions fabric-chaincode-shim/src/test/java/contract/SampleContract.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.hyperledger.fabric.contract.annotation.Contract;
import org.hyperledger.fabric.contract.annotation.Default;
import org.hyperledger.fabric.contract.annotation.Transaction;
import org.hyperledger.fabric.shim.ChaincodeException;

import io.swagger.v3.oas.annotations.info.Contact;
import io.swagger.v3.oas.annotations.info.Info;
Expand Down Expand Up @@ -40,8 +41,16 @@ public String tFour(Context ctx) {
}

@Transaction
public String t3(Context ctx) {
throw new RuntimeException("T3 fail!");
public String t3(Context ctx, String exception, String message) {
if ("TransactionException".equals(exception)) {
if (message.isEmpty()) {
throw new ChaincodeException(null, "T3ERR1");
} else {
throw new ChaincodeException(message, "T3ERR1");
}
} else {
throw new RuntimeException(message);
}
}

@Transaction
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;

import org.hyperledger.fabric.shim.ChaincodeException;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
Expand All @@ -25,7 +26,7 @@ public void createContext() {

@Test
public void unknownTransaction() {
thrown.expect(IllegalStateException.class);
thrown.expect(ChaincodeException.class);
thrown.expectMessage("Undefined contract method called");

ContractInterface c = new ContractInterface() {
Expand Down
Loading

0 comments on commit 25c5be6

Please sign in to comment.