Skip to content
31 changes: 23 additions & 8 deletions exonum-java-binding/core/rust/src/proxy/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* limitations under the License.
*/

use exonum::runtime::Caller;
use exonum::{
blockchain::Blockchain,
crypto::{Hash, PublicKey},
Expand All @@ -26,6 +27,7 @@ use exonum::{
};
use exonum_proto::ProtobufConvert;
use futures::{Future, IntoFuture};
use jni::sys::jint;
use jni::{
objects::{GlobalRef, JObject, JValue},
signature::{JavaType, Primitive},
Expand Down Expand Up @@ -303,10 +305,17 @@ impl Runtime for JavaRuntimeProxy {
call_info: &CallInfo,
arguments: &[u8],
) -> Result<(), ExecutionError> {
let tx = match context.caller.as_transaction() {
Some((hash, pub_key)) => (hash.to_bytes(), pub_key.to_bytes()),
None => {
// TODO (ECR-3702): caller is Service (not Transaction) is not supported yet
// todo: Replace this abomination (8-parameter method, arguments that make sense only
// in some cases) with a single protobuf message or other alternative [ECR-3872]
let tx_info: (InstanceId, Hash, PublicKey) = match context.caller {
Caller::Transaction {
hash: message_hash,
author: author_pk,
} => (0, message_hash, author_pk),
Caller::Service {
instance_id: caller_id,
} => (caller_id, Hash::default(), PublicKey::default()),
Caller::Blockchain => {
return Err(Error::NotSupportedOperation.into());
}
};
Expand All @@ -318,23 +327,29 @@ impl Runtime for JavaRuntimeProxy {
)],
|env| {
let service_id = call_info.instance_id as i32;
let interface_name = JObject::from(env.new_string(context.interface_name)?);
let tx_id = call_info.method_id as i32;
let args = JObject::from(env.byte_array_from_slice(arguments)?);
let view_handle = to_handle(View::from_ref_fork(context.fork));
let hash = JObject::from(env.byte_array_from_slice(&tx.0)?);
let pub_key = JObject::from(env.byte_array_from_slice(&tx.1)?);
let caller_id = tx_info.0;
let message_hash = tx_info.1.to_bytes();
let message_hash = JObject::from(env.byte_array_from_slice(&message_hash)?);
let author_pk = tx_info.2.to_bytes();
let author_pk = JObject::from(env.byte_array_from_slice(&author_pk)?);

env.call_method_unchecked(
self.runtime_adapter.as_obj(),
runtime_adapter::execute_tx_id(),
JavaType::Primitive(Primitive::Void),
&[
JValue::from(service_id),
JValue::from(interface_name),
JValue::from(tx_id),
JValue::from(args),
JValue::from(view_handle),
JValue::from(hash),
JValue::from(pub_key),
JValue::from(caller_id as jint),
JValue::from(message_hash),
JValue::from(author_pk),
],
)
.and_then(JValue::v)
Expand Down
2 changes: 1 addition & 1 deletion exonum-java-binding/core/rust/src/utils/jni_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ unsafe fn cache_methods(env: &JNIEnv) {
&env,
SERVICE_RUNTIME_ADAPTER_CLASS,
"executeTransaction",
"(II[BJ[B[B)V",
"(ILjava/lang/String;I[BJI[B[B)V",
);
RUNTIME_ADAPTER_STATE_HASHES = get_method_id(
&env,
Expand Down
5 changes: 2 additions & 3 deletions exonum-java-binding/core/spotbugs-exclude.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,10 @@
<Bug pattern="NP_PARAMETER_MUST_BE_NONNULL_BUT_MARKED_AS_NULLABLE"/>
</Match>

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,16 +276,22 @@ private void connectServiceApi(ServiceWrapper service) {

/**
* Executes a transaction belonging to the given service.
*
* @param serviceId the numeric identifier of the service instance to which the transaction
* belongs
* @param interfaceName a fully-qualified name of the interface in which the transaction
* is defined, or empty string if it is defined in the service directly (implicit interface)
* @param txId the transaction type identifier
* @param arguments the serialized transaction arguments
* @param fork a native fork object
* @param callerServiceId the id of the caller service if transaction is invoked by other
* service. Currently only applicable to invocations of Configure interface methods
* @param txMessageHash the hash of the transaction message
* @param authorPublicKey the public key of the transaction author
*/
public void executeTransaction(int serviceId, int txId, byte[] arguments,
Fork fork, HashCode txMessageHash, PublicKey authorPublicKey)
public void executeTransaction(int serviceId, String interfaceName, int txId,
byte[] arguments, Fork fork, int callerServiceId, HashCode txMessageHash,
PublicKey authorPublicKey)
throws TransactionExecutionException {
synchronized (lock) {
ServiceWrapper service = getServiceById(serviceId);
Expand All @@ -298,7 +304,7 @@ public void executeTransaction(int serviceId, int txId, byte[] arguments,
.serviceId(serviceId)
.build();
try {
service.executeTransaction(txId, arguments, context);
service.executeTransaction(interfaceName, txId, arguments, callerServiceId, context);
} catch (Exception e) {
logger.info("Transaction execution failed (service={}, txId={}, txMessageHash={})",
service.getName(), txId, context.getTransactionMessageHash(), e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,25 +163,30 @@ private static ServiceInstanceSpec parseInstanceSpec(byte[] instanceSpec) {
* Executes the service transaction.
*
* @param serviceId the service numeric identifier
* @param interfaceName the name of the interface in which the transaction is defined
* @param txId the transaction type identifier within the service
* @param arguments the transaction arguments
* @param forkNativeHandle a handle to a native fork object
* @param callerServiceId the id of the service which invoked the transaction (in case of
* inner transactions); or 0 when the caller is an external message
* @param txMessageHash the hash of the transaction message
* @param authorPublicKey the public key of the transaction author
* @throws TransactionExecutionException if the transaction execution failed
* @see ServiceRuntime#executeTransaction(int, int, byte[], Fork, HashCode, PublicKey)
* @see ServiceRuntime#executeTransaction(int, String, int, byte[], Fork, int, HashCode,
* PublicKey)
* @see com.exonum.binding.core.transaction.Transaction#execute(TransactionContext)
*/
void executeTransaction(int serviceId, int txId, byte[] arguments,
long forkNativeHandle, byte[] txMessageHash, byte[] authorPublicKey)
void executeTransaction(int serviceId, String interfaceName, int txId, byte[] arguments,
long forkNativeHandle, int callerServiceId, byte[] txMessageHash, byte[] authorPublicKey)
throws TransactionExecutionException, CloseFailuresException {

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

serviceRuntime.executeTransaction(serviceId, txId, arguments, fork, hash, authorPk);
serviceRuntime.executeTransaction(serviceId, interfaceName, txId, arguments, fork,
callerServiceId, hash, authorPk);
} catch (CloseFailuresException e) {
handleCloseFailure(e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@

package com.exonum.binding.core.runtime;

import static com.google.common.base.Preconditions.checkArgument;
import static java.lang.String.format;

import com.exonum.binding.common.hash.HashCode;
import com.exonum.binding.common.message.TransactionMessage;
import com.exonum.binding.core.service.BlockCommittedEvent;
import com.exonum.binding.core.service.Configurable;
import com.exonum.binding.core.service.Configuration;
import com.exonum.binding.core.service.Node;
import com.exonum.binding.core.service.Service;
Expand All @@ -28,6 +32,7 @@
import com.exonum.binding.core.transaction.Transaction;
import com.exonum.binding.core.transaction.TransactionContext;
import com.exonum.binding.core.transaction.TransactionExecutionException;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.io.BaseEncoding;
import com.google.common.net.UrlEscapers;
import com.google.inject.Inject;
Expand All @@ -41,6 +46,25 @@
*/
final class ServiceWrapper {

/**
* Default interface comprised of transactions defined in the service implementation
* (intrinsic to this service).
*/
static final String DEFAULT_INTERFACE_NAME = "";
/**
* The id of the supervisor service instance, allowed to invoke configuration operations.
*
* <p>See SUPERVISOR_INSTANCE_ID in Exonum.
*/
static final int SUPERVISOR_SERVICE_ID = 0;

// These constants are defined in this class till a generic approach to invoke interface
// methods is implemented.
// See Configure trait in Exonum.
@VisibleForTesting static final String CONFIGURE_INTERFACE_NAME = "exonum.Configure";
@VisibleForTesting static final int VERIFY_CONFIGURATION_TX_ID = 0;
@VisibleForTesting static final int APPLY_CONFIGURATION_TX_ID = 1;

private final Service service;
private final TransactionConverter txConverter;
private final ServiceInstanceSpec instanceSpec;
Expand Down Expand Up @@ -80,7 +104,24 @@ void initialize(Fork view, Configuration configuration) {
service.initialize(view, configuration);
}

void executeTransaction(int txId, byte[] arguments, TransactionContext context)
void executeTransaction(String interfaceName, int txId, byte[] arguments, int callerServiceId,
TransactionContext context)
throws TransactionExecutionException {
switch (interfaceName) {
case DEFAULT_INTERFACE_NAME: {
executeIntrinsicTransaction(txId, arguments, context);
break;
}
case CONFIGURE_INTERFACE_NAME: {
executeConfigurableTransaction(txId, arguments, callerServiceId, context);
break;
}
default: throw new IllegalArgumentException(
format("Unknown interface (name=%s, txId=%d)", interfaceName, txId));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why static import for String.format? That's inconsistent with the rest of our codebase

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To have it more consice and fit in a single expression. I wouldn't consider the consistency of static imports of a particular method as something to be given higher priority over readability and local consistency.

}
}

private void executeIntrinsicTransaction(int txId, byte[] arguments, TransactionContext context)
throws TransactionExecutionException {
// Decode the transaction data into an executable transaction
Transaction transaction = convertTransaction(txId, arguments);
Expand All @@ -100,18 +141,48 @@ void executeTransaction(int txId, byte[] arguments, TransactionContext context)
* arguments are not valid: e.g., cannot be deserialized, or do not meet the preconditions
*/
Transaction convertTransaction(int txId, byte[] arguments) {
return convertIntrinsicTransaction(txId, arguments);
}

private Transaction convertIntrinsicTransaction(int txId, byte[] arguments) {
Transaction transaction = txConverter.toTransaction(txId, arguments);
if (transaction == null) {
// Use \n in the format string to ensure the message (which is likely recorded
// to the blockchain) stays the same on any platform
throw new NullPointerException(String.format("Invalid service implementation: "
throw new NullPointerException(format("Invalid service implementation: "
+ "TransactionConverter#toTransaction must never return null.\n"
+ "Throw an exception if your service does not recognize this message id (%s) "
+ "or arguments (%s)", txId, BaseEncoding.base16().encode(arguments)));
}
return transaction;
}

private void executeConfigurableTransaction(int txId, byte[] arguments, int callerServiceId,
TransactionContext context) {
// Check the service implements Configurable
checkArgument(service instanceof Configurable, "Service (%s) doesn't implement Configurable",
getName());
// Check the caller is the supervisor
checkArgument(callerServiceId == SUPERVISOR_SERVICE_ID, "Invalid caller service id (%s). "
+ "Operations in Configurable interface may only be invoked by the supervisor service (%s)",
callerServiceId, SUPERVISOR_SERVICE_ID);
// Invoke the Configurable operation
Configurable configurable = (Configurable) service;
Fork fork = context.getFork();
Configuration config = new ServiceConfiguration(arguments);
switch (txId) {
case VERIFY_CONFIGURATION_TX_ID:
configurable.verifyConfiguration(fork, config);
break;
case APPLY_CONFIGURATION_TX_ID:
configurable.applyConfiguration(fork, config);
break;
default:
throw new IllegalArgumentException(
format("Unknown txId (%d) in Configurable interface", txId));
}
}

List<HashCode> getStateHashes(Snapshot snapshot) {
return service.getStateHashes(snapshot);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright 2019 The Exonum Team
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.exonum.binding.core.service;

import com.exonum.binding.core.storage.database.Fork;

/**
* A configurable Exonum service. Allows services to update their configuration through
* the supervisor service during their operation.
*
* <p>The configuration update process includes the following steps: a proposal
* of a new configuration; verification of its correctness; approval of the proposal;
* and application of the new configuration. The protocol of the proposal and approval steps
* is determined by the installed supervisor service. The verification and application
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are determined?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The protocol ... is determined

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, my mistake

* of the parameters are implemented by the service with
* {@link #verifyConfiguration(Fork, Configuration)}
* and {@link #applyConfiguration(Fork, Configuration)} methods.
*
* <p>Services may use the same configuration parameters as
* in {@link Service#initialize(Fork, Configuration)}, or different.
* <!--
* TODO: Link the appropriate documentation section on updating the service configuration
* through the supervisor when it becomes available (ideally, on the site; or in published
* Rust docs)
* -->
*/
public interface Configurable {

/**
* Verifies the correctness of the proposed configuration.
*
* <p>This method is called when a new configuration is proposed. If the proposed
* configuration is correct, this method shall return with no changes to the service data.
* If it is not valid, this method shall throw an exception.
*
* @param fork a view representing the current database state
* @param configuration a proposed configuration
* @throws IllegalArgumentException if the proposed configuration is not valid
*/
void verifyConfiguration(Fork fork, Configuration configuration);

/**
* Applies the given configuration to this service. The configuration is guaranteed to be
* valid according to {@link #verifyConfiguration(Fork, Configuration)}.
*
* <p>The implementation shall make any changes to the service persistent state to apply
* the new configuration, because the supervisor does <em>not</em> store them for later retrieval.
*
* @param fork a fork to which to apply changes
* @param configuration a new valid configuration
*/
void applyConfiguration(Fork fork, Configuration configuration);
}
Loading