Skip to content

Commit 25c5be6

Browse files
committed
[FAB-15615] Add ChaincodeException support
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]>
1 parent 9077581 commit 25c5be6

File tree

12 files changed

+374
-47
lines changed

12 files changed

+374
-47
lines changed

fabric-chaincode-shim/src/main/java/org/hyperledger/fabric/contract/ContractInterface.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import org.hyperledger.fabric.contract.annotation.Contract;
1010
import org.hyperledger.fabric.contract.annotation.Transaction;
1111
import org.hyperledger.fabric.shim.ChaincodeStub;
12+
import org.hyperledger.fabric.shim.ChaincodeException;
1213

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

7879
/**

fabric-chaincode-shim/src/main/java/org/hyperledger/fabric/contract/ContractRouter.java

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -75,25 +75,32 @@ void startRouting() {
7575
}
7676
}
7777

78-
@Override
79-
public Response invoke(ChaincodeStub stub) {
78+
private Response processRequest(ChaincodeStub stub) {
8079
logger.info(() -> "Got invoke routing request");
81-
if (stub.getStringArgs().size() > 0) {
82-
logger.info(() -> "Got the invoke request for:" + stub.getFunction() + " " + stub.getParameters());
83-
InvocationRequest request = ExecutionFactory.getInstance().createRequest(stub);
84-
TxFunction txFn = getRouting(request);
85-
86-
logger.info(() -> "Got routing:" + txFn.getRouting());
87-
return executor.executeRequest(txFn, request, stub);
88-
} else {
89-
return ResponseUtils.newSuccessResponse();
80+
try {
81+
if (stub.getStringArgs().size() > 0) {
82+
logger.info(() -> "Got the invoke request for:" + stub.getFunction() + " " + stub.getParameters());
83+
InvocationRequest request = ExecutionFactory.getInstance().createRequest(stub);
84+
TxFunction txFn = getRouting(request);
85+
86+
logger.info(() -> "Got routing:" + txFn.getRouting());
87+
return executor.executeRequest(txFn, request, stub);
88+
} else {
89+
return ResponseUtils.newSuccessResponse();
90+
}
91+
} catch (Throwable throwable) {
92+
return ResponseUtils.newErrorResponse(throwable);
9093
}
94+
}
9195

96+
@Override
97+
public Response invoke(ChaincodeStub stub) {
98+
return processRequest(stub);
9299
}
93100

94101
@Override
95102
public Response init(ChaincodeStub stub) {
96-
return invoke(stub);
103+
return processRequest(stub);
97104
}
98105

99106
/**

fabric-chaincode-shim/src/main/java/org/hyperledger/fabric/contract/ContractRuntimeException.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
*/
66
package org.hyperledger.fabric.contract;
77

8+
import org.hyperledger.fabric.shim.ChaincodeException;
9+
810
/**
911
* Specific RuntimeException for events that occur in the calling and handling
1012
* of the Contracts, NOT within the contract logic itself.
@@ -13,7 +15,7 @@
1315
* for example current tx id
1416
*
1517
*/
16-
public class ContractRuntimeException extends RuntimeException {
18+
public class ContractRuntimeException extends ChaincodeException {
1719

1820
public ContractRuntimeException(String string) {
1921
super(string);

fabric-chaincode-shim/src/main/java/org/hyperledger/fabric/contract/execution/impl/ContractExecutionService.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import org.hyperledger.fabric.Logger;
1616
import org.hyperledger.fabric.contract.Context;
1717
import org.hyperledger.fabric.contract.ContractInterface;
18+
import org.hyperledger.fabric.contract.ContractRuntimeException;
1819
import org.hyperledger.fabric.contract.execution.ExecutionService;
1920
import org.hyperledger.fabric.contract.execution.InvocationRequest;
2021
import org.hyperledger.fabric.contract.execution.JSONTransactionSerializer;
@@ -23,6 +24,7 @@
2324
import org.hyperledger.fabric.contract.routing.TxFunction;
2425
import org.hyperledger.fabric.contract.routing.TypeRegistry;
2526
import org.hyperledger.fabric.shim.Chaincode;
27+
import org.hyperledger.fabric.shim.ChaincodeException;
2628
import org.hyperledger.fabric.shim.ChaincodeStub;
2729
import org.hyperledger.fabric.shim.ResponseUtils;
2830

@@ -62,11 +64,16 @@ public Chaincode.Response executeRequest(TxFunction txFn, InvocationRequest req,
6264
}
6365

6466
} catch (IllegalAccessException | InstantiationException e) {
65-
logger.error(() -> "Error during contract method invocation" + e);
66-
response = ResponseUtils.newErrorResponse(e);
67+
String message = String.format("Could not execute contract method: %s", rd.toString());
68+
throw new ContractRuntimeException(message, e);
6769
} catch (InvocationTargetException e) {
68-
logger.error(() -> "Error during contract method invocation" + e);
69-
response = ResponseUtils.newErrorResponse(e.getCause());
70+
Throwable cause = e.getCause();
71+
72+
if (cause instanceof ChaincodeException) {
73+
throw (ChaincodeException) cause;
74+
} else {
75+
throw new ContractRuntimeException("Error during contract method execution", cause);
76+
}
7077
}
7178

7279
return response;
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/*
2+
Copyright IBM Corp. All Rights Reserved.
3+
4+
SPDX-License-Identifier: Apache-2.0
5+
*/
6+
package org.hyperledger.fabric.shim;
7+
8+
import static java.nio.charset.StandardCharsets.UTF_8;
9+
10+
/**
11+
* Contracts should use {@code ChaincodeException} to indicate when an error
12+
* occurs in Smart Contract logic.
13+
*
14+
* <p>
15+
* When a {@code ChaincodeException} is thrown an error response will be
16+
* returned from the chaincode container containing the exception message and
17+
* payload, if specified.
18+
*
19+
* <p>
20+
* {@code ChaincodeException} may be extended to provide application specific
21+
* error information. Subclasses should ensure that {@link #getPayload} returns
22+
* a serialized representation of the error in a suitable format for client
23+
* applications to process.
24+
*/
25+
public class ChaincodeException extends RuntimeException {
26+
27+
private static final long serialVersionUID = 3664437023130016393L;
28+
29+
private byte[] payload;
30+
31+
/**
32+
* Constructs a new {@code ChaincodeException} with no detail message.
33+
*/
34+
public ChaincodeException() {
35+
super();
36+
}
37+
38+
/**
39+
* Constructs a new {@code ChaincodeException} with the specified detail
40+
* message.
41+
*
42+
* @param message the detail message.
43+
*/
44+
public ChaincodeException(String message) {
45+
super(message);
46+
}
47+
48+
/**
49+
* Constructs a new {@code ChaincodeException} with the specified cause.
50+
*
51+
* @param cause the cause.
52+
*/
53+
public ChaincodeException(Throwable cause) {
54+
super(cause);
55+
}
56+
57+
/**
58+
* Constructs a new {@code ChaincodeException} with the specified detail
59+
* message and cause.
60+
*
61+
* @param message the detail message.
62+
* @param cause the cause.
63+
*/
64+
public ChaincodeException(String message, Throwable cause) {
65+
super(message, cause);
66+
}
67+
68+
/**
69+
* Constructs a new {@code ChaincodeException} with the specified detail
70+
* message and response payload.
71+
*
72+
* @param message the detail message.
73+
* @param payload the response payload.
74+
*/
75+
public ChaincodeException(String message, byte[] payload) {
76+
super(message);
77+
78+
this.payload = payload;
79+
}
80+
81+
/**
82+
* Constructs a new {@code ChaincodeException} with the specified detail
83+
* message, response payload and cause.
84+
*
85+
* @param message the detail message.
86+
* @param payload the response payload.
87+
* @param cause the cause.
88+
*/
89+
public ChaincodeException(String message, byte[] payload, Throwable cause) {
90+
super(message, cause);
91+
92+
this.payload = payload;
93+
}
94+
95+
/**
96+
* Constructs a new {@code ChaincodeException} with the specified detail
97+
* message and response payload.
98+
*
99+
* @param message the detail message.
100+
* @param payload the response payload.
101+
*/
102+
public ChaincodeException(String message, String payload) {
103+
super(message);
104+
105+
this.payload = payload.getBytes(UTF_8);
106+
}
107+
108+
/**
109+
* Constructs a new {@code ChaincodeException} with the specified detail
110+
* message, response payload and cause.
111+
*
112+
* @param message the detail message.
113+
* @param payload the response payload.
114+
* @param cause the cause.
115+
*/
116+
public ChaincodeException(String message, String payload, Throwable cause) {
117+
super(message, cause);
118+
119+
this.payload = payload.getBytes(UTF_8);
120+
}
121+
122+
/**
123+
* Returns the response payload or {@code null} if there is no response.
124+
*
125+
* <p>
126+
* The payload should represent the chaincode error in a way that client
127+
* applications written in different programming languages can interpret. For
128+
* example it could include a domain specific error code, in addition to any
129+
* state information which would allow client applications to respond
130+
* appropriately.
131+
*
132+
* @return the response payload or {@code null} if there is no response.
133+
*/
134+
public byte[] getPayload() {
135+
return payload;
136+
}
137+
}

fabric-chaincode-shim/src/main/java/org/hyperledger/fabric/shim/ResponseUtils.java

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@
55
*/
66
package org.hyperledger.fabric.shim;
77

8-
import java.io.PrintWriter;
9-
import java.io.StringWriter;
10-
import java.nio.charset.StandardCharsets;
11-
128
import static org.hyperledger.fabric.shim.Chaincode.Response.Status.INTERNAL_SERVER_ERROR;
139
import static org.hyperledger.fabric.shim.Chaincode.Response.Status.SUCCESS;
1410

11+
import org.hyperledger.fabric.Logger;
12+
1513
public class ResponseUtils {
14+
15+
private static Logger logger = Logger.getLogger(ResponseUtils.class.getName());
16+
1617
public static Chaincode.Response newSuccessResponse(String message, byte[] payload) {
1718
return new Chaincode.Response(SUCCESS, message, payload);
1819
}
@@ -46,14 +47,18 @@ public static Chaincode.Response newErrorResponse(byte[] payload) {
4647
}
4748

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

52-
private static byte[] printStackTrace(Throwable throwable) {
53-
if (throwable == null) return null;
54-
final StringWriter buffer = new StringWriter();
55-
throwable.printStackTrace(new PrintWriter(buffer));
56-
return buffer.toString().getBytes(StandardCharsets.UTF_8);
57-
}
53+
String message = null;
54+
byte[] payload = null;
55+
if (throwable instanceof ChaincodeException) {
56+
message = throwable.getMessage();
57+
payload = ((ChaincodeException) throwable).getPayload();
58+
} else {
59+
message = "Unexpected error";
60+
}
5861

62+
return ResponseUtils.newErrorResponse(message, payload);
63+
}
5964
}

fabric-chaincode-shim/src/test/java/contract/SampleContract.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.hyperledger.fabric.contract.annotation.Contract;
1313
import org.hyperledger.fabric.contract.annotation.Default;
1414
import org.hyperledger.fabric.contract.annotation.Transaction;
15+
import org.hyperledger.fabric.shim.ChaincodeException;
1516

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

4243
@Transaction
43-
public String t3(Context ctx) {
44-
throw new RuntimeException("T3 fail!");
44+
public String t3(Context ctx, String exception, String message) {
45+
if ("TransactionException".equals(exception)) {
46+
if (message.isEmpty()) {
47+
throw new ChaincodeException(null, "T3ERR1");
48+
} else {
49+
throw new ChaincodeException(message, "T3ERR1");
50+
}
51+
} else {
52+
throw new RuntimeException(message);
53+
}
4554
}
4655

4756
@Transaction

fabric-chaincode-shim/src/test/java/org/hyperledger/fabric/contract/ContractInterfaceTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import static org.hamcrest.Matchers.is;
1010
import static org.junit.Assert.assertThat;
1111

12+
import org.hyperledger.fabric.shim.ChaincodeException;
1213
import org.junit.Rule;
1314
import org.junit.Test;
1415
import org.junit.rules.ExpectedException;
@@ -25,7 +26,7 @@ public void createContext() {
2526

2627
@Test
2728
public void unknownTransaction() {
28-
thrown.expect(IllegalStateException.class);
29+
thrown.expect(ChaincodeException.class);
2930
thrown.expectMessage("Undefined contract method called");
3031

3132
ContractInterface c = new ContractInterface() {

0 commit comments

Comments
 (0)