Skip to content

Commit 92de342

Browse files
Add Configurable interface [ECR-3590] (#1234)
Added Configurable interface corresponding to `exonum.Configure` and its support to ServiceWrapper. As the Rust runtime, the Java runtime also performs caller authorization, allowing only the supervisor service to invoke operations of Configurable interface.
1 parent 6af2536 commit 92de342

File tree

9 files changed

+339
-35
lines changed

9 files changed

+339
-35
lines changed

exonum-java-binding/core/rust/src/proxy/runtime.rs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17+
use exonum::runtime::Caller;
1718
use exonum::{
1819
blockchain::Blockchain,
1920
crypto::{Hash, PublicKey},
@@ -26,6 +27,7 @@ use exonum::{
2627
};
2728
use exonum_proto::ProtobufConvert;
2829
use futures::{Future, IntoFuture};
30+
use jni::sys::jint;
2931
use jni::{
3032
objects::{GlobalRef, JObject, JValue},
3133
signature::{JavaType, Primitive},
@@ -303,10 +305,17 @@ impl Runtime for JavaRuntimeProxy {
303305
call_info: &CallInfo,
304306
arguments: &[u8],
305307
) -> Result<(), ExecutionError> {
306-
let tx = match context.caller.as_transaction() {
307-
Some((hash, pub_key)) => (hash.to_bytes(), pub_key.to_bytes()),
308-
None => {
309-
// TODO (ECR-3702): caller is Service (not Transaction) is not supported yet
308+
// todo: Replace this abomination (8-parameter method, arguments that make sense only
309+
// in some cases) with a single protobuf message or other alternative [ECR-3872]
310+
let tx_info: (InstanceId, Hash, PublicKey) = match context.caller {
311+
Caller::Transaction {
312+
hash: message_hash,
313+
author: author_pk,
314+
} => (0, message_hash, author_pk),
315+
Caller::Service {
316+
instance_id: caller_id,
317+
} => (caller_id, Hash::default(), PublicKey::default()),
318+
Caller::Blockchain => {
310319
return Err(Error::NotSupportedOperation.into());
311320
}
312321
};
@@ -318,23 +327,29 @@ impl Runtime for JavaRuntimeProxy {
318327
)],
319328
|env| {
320329
let service_id = call_info.instance_id as i32;
330+
let interface_name = JObject::from(env.new_string(context.interface_name)?);
321331
let tx_id = call_info.method_id as i32;
322332
let args = JObject::from(env.byte_array_from_slice(arguments)?);
323333
let view_handle = to_handle(View::from_ref_fork(context.fork));
324-
let hash = JObject::from(env.byte_array_from_slice(&tx.0)?);
325-
let pub_key = JObject::from(env.byte_array_from_slice(&tx.1)?);
334+
let caller_id = tx_info.0;
335+
let message_hash = tx_info.1.to_bytes();
336+
let message_hash = JObject::from(env.byte_array_from_slice(&message_hash)?);
337+
let author_pk = tx_info.2.to_bytes();
338+
let author_pk = JObject::from(env.byte_array_from_slice(&author_pk)?);
326339

327340
env.call_method_unchecked(
328341
self.runtime_adapter.as_obj(),
329342
runtime_adapter::execute_tx_id(),
330343
JavaType::Primitive(Primitive::Void),
331344
&[
332345
JValue::from(service_id),
346+
JValue::from(interface_name),
333347
JValue::from(tx_id),
334348
JValue::from(args),
335349
JValue::from(view_handle),
336-
JValue::from(hash),
337-
JValue::from(pub_key),
350+
JValue::from(caller_id as jint),
351+
JValue::from(message_hash),
352+
JValue::from(author_pk),
338353
],
339354
)
340355
.and_then(JValue::v)

exonum-java-binding/core/rust/src/utils/jni_cache.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ unsafe fn cache_methods(env: &JNIEnv) {
120120
&env,
121121
SERVICE_RUNTIME_ADAPTER_CLASS,
122122
"executeTransaction",
123-
"(II[BJ[B[B)V",
123+
"(ILjava/lang/String;I[BJI[B[B)V",
124124
);
125125
RUNTIME_ADAPTER_STATE_HASHES = get_method_id(
126126
&env,

exonum-java-binding/core/spotbugs-exclude.xml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,10 @@
1212
<Bug pattern="NP_PARAMETER_MUST_BE_NONNULL_BUT_MARKED_AS_NULLABLE"/>
1313
</Match>
1414

15-
<!-- Use \n in the message that might be recorded in the blockchain instead of %n, which
16-
is resolved to a platform-dependent value. -->
15+
<!-- Use \n in the exception message that might be recorded in the blockchain instead of %n,
16+
which is resolved to a platform-dependent value. -->
1717
<Match>
1818
<Class name="~.*ServiceWrapper"/>
19-
<Method name="convertTransaction"/>
2019
<Bug pattern="VA_FORMAT_STRING_USES_NEWLINE"/>
2120
</Match>
2221

exonum-java-binding/core/src/main/java/com/exonum/binding/core/runtime/ServiceRuntime.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -276,16 +276,22 @@ private void connectServiceApi(ServiceWrapper service) {
276276

277277
/**
278278
* Executes a transaction belonging to the given service.
279+
*
279280
* @param serviceId the numeric identifier of the service instance to which the transaction
280281
* belongs
282+
* @param interfaceName a fully-qualified name of the interface in which the transaction
283+
* is defined, or empty string if it is defined in the service directly (implicit interface)
281284
* @param txId the transaction type identifier
282285
* @param arguments the serialized transaction arguments
283286
* @param fork a native fork object
287+
* @param callerServiceId the id of the caller service if transaction is invoked by other
288+
* service. Currently only applicable to invocations of Configure interface methods
284289
* @param txMessageHash the hash of the transaction message
285290
* @param authorPublicKey the public key of the transaction author
286291
*/
287-
public void executeTransaction(int serviceId, int txId, byte[] arguments,
288-
Fork fork, HashCode txMessageHash, PublicKey authorPublicKey)
292+
public void executeTransaction(int serviceId, String interfaceName, int txId,
293+
byte[] arguments, Fork fork, int callerServiceId, HashCode txMessageHash,
294+
PublicKey authorPublicKey)
289295
throws TransactionExecutionException {
290296
synchronized (lock) {
291297
ServiceWrapper service = getServiceById(serviceId);
@@ -298,7 +304,7 @@ public void executeTransaction(int serviceId, int txId, byte[] arguments,
298304
.serviceId(serviceId)
299305
.build();
300306
try {
301-
service.executeTransaction(txId, arguments, context);
307+
service.executeTransaction(interfaceName, txId, arguments, callerServiceId, context);
302308
} catch (Exception e) {
303309
logger.info("Transaction execution failed (service={}, txId={}, txMessageHash={})",
304310
service.getName(), txId, context.getTransactionMessageHash(), e);

exonum-java-binding/core/src/main/java/com/exonum/binding/core/runtime/ServiceRuntimeAdapter.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,25 +163,30 @@ private static ServiceInstanceSpec parseInstanceSpec(byte[] instanceSpec) {
163163
* Executes the service transaction.
164164
*
165165
* @param serviceId the service numeric identifier
166+
* @param interfaceName the name of the interface in which the transaction is defined
166167
* @param txId the transaction type identifier within the service
167168
* @param arguments the transaction arguments
168169
* @param forkNativeHandle a handle to a native fork object
170+
* @param callerServiceId the id of the service which invoked the transaction (in case of
171+
* inner transactions); or 0 when the caller is an external message
169172
* @param txMessageHash the hash of the transaction message
170173
* @param authorPublicKey the public key of the transaction author
171174
* @throws TransactionExecutionException if the transaction execution failed
172-
* @see ServiceRuntime#executeTransaction(int, int, byte[], Fork, HashCode, PublicKey)
175+
* @see ServiceRuntime#executeTransaction(int, String, int, byte[], Fork, int, HashCode,
176+
* PublicKey)
173177
* @see com.exonum.binding.core.transaction.Transaction#execute(TransactionContext)
174178
*/
175-
void executeTransaction(int serviceId, int txId, byte[] arguments,
176-
long forkNativeHandle, byte[] txMessageHash, byte[] authorPublicKey)
179+
void executeTransaction(int serviceId, String interfaceName, int txId, byte[] arguments,
180+
long forkNativeHandle, int callerServiceId, byte[] txMessageHash, byte[] authorPublicKey)
177181
throws TransactionExecutionException, CloseFailuresException {
178182

179183
try (Cleaner cleaner = new Cleaner("executeTransaction")) {
180184
Fork fork = viewFactory.createFork(forkNativeHandle, cleaner);
181185
HashCode hash = HashCode.fromBytes(txMessageHash);
182186
PublicKey authorPk = PublicKey.fromBytes(authorPublicKey);
183187

184-
serviceRuntime.executeTransaction(serviceId, txId, arguments, fork, hash, authorPk);
188+
serviceRuntime.executeTransaction(serviceId, interfaceName, txId, arguments, fork,
189+
callerServiceId, hash, authorPk);
185190
} catch (CloseFailuresException e) {
186191
handleCloseFailure(e);
187192
}

exonum-java-binding/core/src/main/java/com/exonum/binding/core/runtime/ServiceWrapper.java

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@
1616

1717
package com.exonum.binding.core.runtime;
1818

19+
import static com.google.common.base.Preconditions.checkArgument;
20+
import static java.lang.String.format;
21+
1922
import com.exonum.binding.common.hash.HashCode;
2023
import com.exonum.binding.common.message.TransactionMessage;
2124
import com.exonum.binding.core.service.BlockCommittedEvent;
25+
import com.exonum.binding.core.service.Configurable;
2226
import com.exonum.binding.core.service.Configuration;
2327
import com.exonum.binding.core.service.Node;
2428
import com.exonum.binding.core.service.Service;
@@ -28,6 +32,7 @@
2832
import com.exonum.binding.core.transaction.Transaction;
2933
import com.exonum.binding.core.transaction.TransactionContext;
3034
import com.exonum.binding.core.transaction.TransactionExecutionException;
35+
import com.google.common.annotations.VisibleForTesting;
3136
import com.google.common.io.BaseEncoding;
3237
import com.google.common.net.UrlEscapers;
3338
import com.google.inject.Inject;
@@ -41,6 +46,25 @@
4146
*/
4247
final class ServiceWrapper {
4348

49+
/**
50+
* Default interface comprised of transactions defined in the service implementation
51+
* (intrinsic to this service).
52+
*/
53+
static final String DEFAULT_INTERFACE_NAME = "";
54+
/**
55+
* The id of the supervisor service instance, allowed to invoke configuration operations.
56+
*
57+
* <p>See SUPERVISOR_INSTANCE_ID in Exonum.
58+
*/
59+
static final int SUPERVISOR_SERVICE_ID = 0;
60+
61+
// These constants are defined in this class till a generic approach to invoke interface
62+
// methods is implemented.
63+
// See Configure trait in Exonum.
64+
@VisibleForTesting static final String CONFIGURE_INTERFACE_NAME = "exonum.Configure";
65+
@VisibleForTesting static final int VERIFY_CONFIGURATION_TX_ID = 0;
66+
@VisibleForTesting static final int APPLY_CONFIGURATION_TX_ID = 1;
67+
4468
private final Service service;
4569
private final TransactionConverter txConverter;
4670
private final ServiceInstanceSpec instanceSpec;
@@ -80,7 +104,24 @@ void initialize(Fork view, Configuration configuration) {
80104
service.initialize(view, configuration);
81105
}
82106

83-
void executeTransaction(int txId, byte[] arguments, TransactionContext context)
107+
void executeTransaction(String interfaceName, int txId, byte[] arguments, int callerServiceId,
108+
TransactionContext context)
109+
throws TransactionExecutionException {
110+
switch (interfaceName) {
111+
case DEFAULT_INTERFACE_NAME: {
112+
executeIntrinsicTransaction(txId, arguments, context);
113+
break;
114+
}
115+
case CONFIGURE_INTERFACE_NAME: {
116+
executeConfigurableTransaction(txId, arguments, callerServiceId, context);
117+
break;
118+
}
119+
default: throw new IllegalArgumentException(
120+
format("Unknown interface (name=%s, txId=%d)", interfaceName, txId));
121+
}
122+
}
123+
124+
private void executeIntrinsicTransaction(int txId, byte[] arguments, TransactionContext context)
84125
throws TransactionExecutionException {
85126
// Decode the transaction data into an executable transaction
86127
Transaction transaction = convertTransaction(txId, arguments);
@@ -100,18 +141,48 @@ void executeTransaction(int txId, byte[] arguments, TransactionContext context)
100141
* arguments are not valid: e.g., cannot be deserialized, or do not meet the preconditions
101142
*/
102143
Transaction convertTransaction(int txId, byte[] arguments) {
144+
return convertIntrinsicTransaction(txId, arguments);
145+
}
146+
147+
private Transaction convertIntrinsicTransaction(int txId, byte[] arguments) {
103148
Transaction transaction = txConverter.toTransaction(txId, arguments);
104149
if (transaction == null) {
105150
// Use \n in the format string to ensure the message (which is likely recorded
106151
// to the blockchain) stays the same on any platform
107-
throw new NullPointerException(String.format("Invalid service implementation: "
152+
throw new NullPointerException(format("Invalid service implementation: "
108153
+ "TransactionConverter#toTransaction must never return null.\n"
109154
+ "Throw an exception if your service does not recognize this message id (%s) "
110155
+ "or arguments (%s)", txId, BaseEncoding.base16().encode(arguments)));
111156
}
112157
return transaction;
113158
}
114159

160+
private void executeConfigurableTransaction(int txId, byte[] arguments, int callerServiceId,
161+
TransactionContext context) {
162+
// Check the service implements Configurable
163+
checkArgument(service instanceof Configurable, "Service (%s) doesn't implement Configurable",
164+
getName());
165+
// Check the caller is the supervisor
166+
checkArgument(callerServiceId == SUPERVISOR_SERVICE_ID, "Invalid caller service id (%s). "
167+
+ "Operations in Configurable interface may only be invoked by the supervisor service (%s)",
168+
callerServiceId, SUPERVISOR_SERVICE_ID);
169+
// Invoke the Configurable operation
170+
Configurable configurable = (Configurable) service;
171+
Fork fork = context.getFork();
172+
Configuration config = new ServiceConfiguration(arguments);
173+
switch (txId) {
174+
case VERIFY_CONFIGURATION_TX_ID:
175+
configurable.verifyConfiguration(fork, config);
176+
break;
177+
case APPLY_CONFIGURATION_TX_ID:
178+
configurable.applyConfiguration(fork, config);
179+
break;
180+
default:
181+
throw new IllegalArgumentException(
182+
format("Unknown txId (%d) in Configurable interface", txId));
183+
}
184+
}
185+
115186
List<HashCode> getStateHashes(Snapshot snapshot) {
116187
return service.getStateHashes(snapshot);
117188
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright 2019 The Exonum Team
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.exonum.binding.core.service;
18+
19+
import com.exonum.binding.core.storage.database.Fork;
20+
21+
/**
22+
* A configurable Exonum service. Allows services to update their configuration through
23+
* the supervisor service during their operation.
24+
*
25+
* <p>The configuration update process includes the following steps: a proposal
26+
* of a new configuration; verification of its correctness; approval of the proposal;
27+
* and application of the new configuration. The protocol of the proposal and approval steps
28+
* is determined by the installed supervisor service. The verification and application
29+
* of the parameters are implemented by the service with
30+
* {@link #verifyConfiguration(Fork, Configuration)}
31+
* and {@link #applyConfiguration(Fork, Configuration)} methods.
32+
*
33+
* <p>Services may use the same configuration parameters as
34+
* in {@link Service#initialize(Fork, Configuration)}, or different.
35+
* <!--
36+
* TODO: Link the appropriate documentation section on updating the service configuration
37+
* through the supervisor when it becomes available (ideally, on the site; or in published
38+
* Rust docs)
39+
* -->
40+
*/
41+
public interface Configurable {
42+
43+
/**
44+
* Verifies the correctness of the proposed configuration.
45+
*
46+
* <p>This method is called when a new configuration is proposed. If the proposed
47+
* configuration is correct, this method shall return with no changes to the service data.
48+
* If it is not valid, this method shall throw an exception.
49+
*
50+
* @param fork a view representing the current database state
51+
* @param configuration a proposed configuration
52+
* @throws IllegalArgumentException if the proposed configuration is not valid
53+
*/
54+
void verifyConfiguration(Fork fork, Configuration configuration);
55+
56+
/**
57+
* Applies the given configuration to this service. The configuration is guaranteed to be
58+
* valid according to {@link #verifyConfiguration(Fork, Configuration)}.
59+
*
60+
* <p>The implementation shall make any changes to the service persistent state to apply
61+
* the new configuration, because the supervisor does <em>not</em> store them for later retrieval.
62+
*
63+
* @param fork a fork to which to apply changes
64+
* @param configuration a new valid configuration
65+
*/
66+
void applyConfiguration(Fork fork, Configuration configuration);
67+
}

0 commit comments

Comments
 (0)