diff --git a/CHANGELOG.md b/CHANGELOG.md index 41544f682..4550bb5aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning]. ### Added +- Added Java bindings. + [#455](https://github.com/ethereum/evmc/pull/455) - Added `MockedHost` C++ class (in form of header-only `evmc::mocked_host` library) which can be used to emulate Host behavior when testing VM implementations. [#456](https://github.com/ethereum/evmc/pull/456) diff --git a/README.md b/README.md index 9ca49296b..627a20ff6 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Please visit the [documentation]. | **C++** | C++11, C++14, C++17 | GCC 6+, clang 3.8+, MSVC 2015+ | **Go** _(bindings)_ | 1.9 - 1.12 | | **Rust** _(bindings)_[ยน](#n1) | 2018 edition | 1.37.0 and newer +| **Java** _(bindings)_ | 11 | 1. Rust support is limited and not complete yet, but it is mostly functional already. Breaking changes are possible at this stage. diff --git a/bindings/java/.gitignore b/bindings/java/.gitignore new file mode 100644 index 000000000..926d4822c --- /dev/null +++ b/bindings/java/.gitignore @@ -0,0 +1,23 @@ +.idea/ +.vscode +java/build/* +java/out/* +c/build/* +*.o +*.dylib +.DS_Store +.gradle +build +.classpath +.project +.settings/ +java/.classpath +java/.project +java/.settings/ +java/bin/ +java/hs_err_pid* +gradle/ +gradlew +gradlew.bat +c/evmc-vm.h +*.class diff --git a/bindings/java/Makefile b/bindings/java/Makefile new file mode 100644 index 000000000..8da4cb8b6 --- /dev/null +++ b/bindings/java/Makefile @@ -0,0 +1,46 @@ +OS:=$(shell uname -s | tr '[:upper:]' '[:lower:]') +ifeq ($(OS), linux) + EXT:=so + OS_LFLAGS:= + JAVA_HOME:=/usr/local/openjdk-11 +else ifeq ($(OS), darwin) + EXT:=so + OS_LFLAGS:=-mmacosx-version-min=$(shell defaults read loginwindow SystemVersionStampAsString) -framework CoreFoundation -framework Security + JAVA_HOME:=$(shell java -XshowSettings:properties -version 2>&1 > /dev/null | grep 'java.home' | sed 's/\s*java.home = //' | sed 's/\/jre//') +endif + +INCLUDES = -I../../include +JAVA_INCLUDES = -I$(JAVA_HOME)/include/$(OS) -I$(JAVA_HOME)/include +JAVA_LIBS = -L$(JAVA_HOME)/lib/server -ljvm +CFLAGS = -O2 -fPIC +LFLAGS = -shared +OUT_DIR = ./c/build + +gradlew: + gradle setup + +build: gradlew + mkdir -p $(OUT_DIR) + javac ./java/src/main/java/org/ethereum/evmc/EvmcVm.java -h ./c --class-path ./java/src/main/java -s ./java/build + mv c/org_ethereum_evmc_EvmcVm.h c/evmc-vm.h + gcc $(DEBUG_FLAG) $(CFLAGS) -c $(INCLUDES) -o $(OUT_DIR)/loader.o ../../lib/loader/loader.c + gcc $(DEBUG_FLAG) $(CFLAGS) -c $(INCLUDES) $(JAVA_INCLUDES) -o $(OUT_DIR)/host.o ./c/host.c + gcc $(DEBUG_FLAG) $(CFLAGS) ./c/evmc-vm.c $(INCLUDES) $(JAVA_INCLUDES) $(JAVA_LIBS) $(CFLAGS) $(LFLAGS) -o $(OUT_DIR)/evmc.$(EXT) $(OUT_DIR)/host.o $(OUT_DIR)/loader.o + gcc $(DEBUG_FLAG) -shared ../../examples/example_vm/example_vm.c $(INCLUDES) -o $(OUT_DIR)/example_vm.$(EXT) + mkdir -p ./java/build + ./gradlew --no-daemon clean spotlessApply build + +debug: DEBUG_FLAG = -D DEBUG + +debug: build + +test: build + ./gradlew --no-daemon test + +format: + clang-format -i c/evmc-vm.c c/host.c c/host.h + +clean: + rm -rf build + rm -rf ./java/build/ + rm -rf ./c/build/ diff --git a/bindings/java/build.gradle b/bindings/java/build.gradle new file mode 100644 index 000000000..fb20598f5 --- /dev/null +++ b/bindings/java/build.gradle @@ -0,0 +1,40 @@ +plugins { + id 'com.diffplug.gradle.spotless' version '3.16.0' +} +apply from: "${rootDir}/wrapper.gradle" +allprojects { + apply plugin: 'java-library' + repositories { + google() + jcenter() + mavenCentral() + } + sourceCompatibility = '11' + targetCompatibility = '11' + apply plugin: 'com.diffplug.gradle.spotless' + spotless { + java { + // This path needs to be relative to each project + target fileTree('.') { + include '**/*.java' + exclude '**/.gradle/**' + } + importOrder 'org.ethereum', 'java', '' + trimTrailingWhitespace() + endWithNewline() + googleJavaFormat('1.7') + } + } +} + +subprojects { + tasks.withType(Test) { + testLogging.showStandardStreams = true + // If GRADLE_MAX_TEST_FORKS is not set, use half the available processors + maxParallelForks = (System.getenv('GRADLE_MAX_TEST_FORKS') ?: (Runtime.runtime.availableProcessors().intdiv(2) ?: 1)).toInteger() + useJUnitPlatform() + reports { + junitXml.enabled = true + } + } +} diff --git a/bindings/java/c/evmc-vm.c b/bindings/java/c/evmc-vm.c new file mode 100644 index 000000000..b05bf3e51 --- /dev/null +++ b/bindings/java/c/evmc-vm.c @@ -0,0 +1,163 @@ +#include +#include +#include + +#include "evmc-vm.h" +#include "evmc/helpers.h" +#include "evmc/loader.h" +#include "host.h" + +JNIEXPORT jobject JNICALL Java_org_ethereum_evmc_EvmcVm_init(JNIEnv* jenv, + jclass jcls, + jstring jfilename) +{ + struct evmc_vm* evm; + jint rs = set_jvm(jenv); + assert(rs == JNI_OK); + // load the EVM + const char* filename = (*jenv)->GetStringUTFChars(jenv, jfilename, 0); + if (filename != NULL) + { + enum evmc_loader_error_code loader_error; + evm = evmc_load_and_create(filename, &loader_error); + if (evm == NULL || loader_error != EVMC_LOADER_SUCCESS) + { + const char* error_msg = evmc_last_error_msg(); + jclass jclazz = (*jenv)->FindClass(jenv, "java/lang/AssertionError"); + (*jenv)->ThrowNew(jenv, jclazz, error_msg); + } + (*jenv)->ReleaseStringUTFChars(jenv, jfilename, filename); + } + else + { + jclass jclazz = (*jenv)->FindClass(jenv, "java/lang/AssertionError"); + (*jenv)->ThrowNew(jenv, jclazz, "JNI Error: filename cannot be NULL. \n"); + } + jobject jresult = (*jenv)->NewDirectByteBuffer(jenv, (void*)evm, sizeof(struct evmc_vm)); + assert(jresult != NULL); + return jresult; +} + +JNIEXPORT jint JNICALL Java_org_ethereum_evmc_EvmcVm_abi_1version(JNIEnv* jenv, jclass jcls) +{ + return EVMC_ABI_VERSION; +} + +JNIEXPORT jstring JNICALL Java_org_ethereum_evmc_EvmcVm_name(JNIEnv* jenv, + jclass jcls, + jobject jevm) +{ + struct evmc_vm* evm = (struct evmc_vm*)(*jenv)->GetDirectBufferAddress(jenv, jevm); + assert(evm != NULL); + const char* evm_name = evmc_vm_name(evm); + return (*jenv)->NewStringUTF(jenv, evm_name); +} + +JNIEXPORT jstring JNICALL Java_org_ethereum_evmc_EvmcVm_version(JNIEnv* jenv, + jclass jcls, + jobject jevm) +{ + struct evmc_vm* evm = (struct evmc_vm*)(*jenv)->GetDirectBufferAddress(jenv, jevm); + assert(evm != NULL); + const char* evm_version = evmc_vm_version(evm); + return (*jenv)->NewStringUTF(jenv, evm_version); +} + +JNIEXPORT void JNICALL Java_org_ethereum_evmc_EvmcVm_destroy(JNIEnv* jenv, + jclass jcls, + jobject jevm) +{ + struct evmc_vm* evm = (struct evmc_vm*)(*jenv)->GetDirectBufferAddress(jenv, jevm); + assert(evm != NULL); + evmc_destroy(evm); +} + +JNIEXPORT void JNICALL Java_org_ethereum_evmc_EvmcVm_execute(JNIEnv* jenv, + jclass jcls, + jobject jevm, + jint jcontext_index, + jint jrev, + jobject jmsg, + jobject jcode, + jint jsize, + jobject jresult) +{ + struct evmc_message* cmsg = (struct evmc_message*)(*jenv)->GetDirectBufferAddress(jenv, jmsg); + assert(cmsg != NULL); + const uint8_t* ccode = (uint8_t*)(*jenv)->GetDirectBufferAddress(jenv, jcode); + assert(ccode != NULL); + struct evmc_host_context context = {jcontext_index}; + struct evmc_vm* evm = (struct evmc_vm*)(*jenv)->GetDirectBufferAddress(jenv, jevm); + assert(evm != NULL); +#ifdef DEBUG + printf("********************before execute*******************\n"); + + printf("struct: evmc_message=%p\n", cmsg); + printf("sizeof(evmc_message): %lu\n", sizeof(struct evmc_message)); + printf("kind=%p\n", &cmsg->kind); + printf("flags=%p\n", &cmsg->flags); + printf("depth=%p\n", &cmsg->depth); + printf("gas=%p\n", &cmsg->gas); + printf("destination=%p\n", &cmsg->destination.bytes); + printf("sender=%p\n", &cmsg->sender.bytes); + printf("input_data=%p\n", &cmsg->input_data); + printf("input_size=%p\n", &cmsg->input_size); + printf("value=%p\n", &cmsg->value.bytes); + printf("create2_salt=%p\n\n", &cmsg->create2_salt.bytes); + + printf("kind=%d\n", cmsg->kind); + printf("flags=%d\n", cmsg->flags); + printf("depth=%d\n", cmsg->depth); + printf("gas=%lld\n", cmsg->gas); + printf("destination=%s\n", cmsg->destination.bytes); + printf("sender=%s\n", cmsg->sender.bytes); + printf("input_size=%zu\n", cmsg->input_size); + printf("value=%s\n\n", cmsg->value.bytes); +#endif + const struct evmc_host_interface* host = get_host_interface(); + struct evmc_result* result = + (struct evmc_result*)(*jenv)->GetDirectBufferAddress(jenv, jresult); + assert(result != NULL); + *result = evmc_execute(evm, host, &context, jrev, cmsg, ccode, jsize); +#ifdef DEBUG + printf("********************after execute*******************\n"); + printf("sizeof(evmc_result): %lu\n", sizeof(struct evmc_result)); + printf("status_code=%p\n", &result->status_code); + printf("gas_left=%p\n", &result->gas_left); + printf("output_data=%p\n\n", &result->output_data); + + printf("status_code=%d\n", result->status_code); + printf("gas_left=%llu\n", result->gas_left); + printf("output_data=%s\n\n", result->output_data); +#endif +} + +JNIEXPORT jint JNICALL Java_org_ethereum_evmc_EvmcVm_get_1capabilities(JNIEnv* jenv, + jclass jcls, + jobject jevm) +{ + struct evmc_vm* evm = (struct evmc_vm*)(*jenv)->GetDirectBufferAddress(jenv, jevm); + assert(evm != NULL); + return evm->get_capabilities(evm); +} + +JNIEXPORT jint JNICALL Java_org_ethereum_evmc_EvmcVm_set_1option(JNIEnv* jenv, + jclass jcls, + jobject jevm, + jstring jname, + jstring jvalue) +{ + struct evmc_vm* evm = (struct evmc_vm*)(*jenv)->GetDirectBufferAddress(jenv, jevm); + assert(evm != NULL); + const char* name = (*jenv)->GetStringUTFChars(jenv, jname, 0); + const char* value = (*jenv)->GetStringUTFChars(jenv, jvalue, 0); + enum evmc_set_option_result option_result = evmc_set_option(evm, name, value); + (*jenv)->ReleaseStringUTFChars(jenv, jname, name); + (*jenv)->ReleaseStringUTFChars(jenv, jvalue, value); + return option_result; +} + +JNIEXPORT jint JNICALL Java_org_ethereum_evmc_EvmcVm_get_1result_1size(JNIEnv* jenv, jclass jcls) +{ + return sizeof(struct evmc_result); +} diff --git a/bindings/java/c/host.c b/bindings/java/c/host.c new file mode 100644 index 000000000..c44f33dda --- /dev/null +++ b/bindings/java/c/host.c @@ -0,0 +1,584 @@ +#include +#include + +#include "host.h" + +static JavaVM* jvm; + +int set_jvm(JNIEnv* jenv) +{ + return (*jenv)->GetJavaVM(jenv, &jvm); +} + +static JNIEnv* attach() +{ + JNIEnv* jenv; + jint rs = (*jvm)->AttachCurrentThread(jvm, (void**)&jenv, NULL); + assert(rs == JNI_OK); + return jenv; +} + +static bool account_exists_fn(struct evmc_host_context* context, const evmc_address* address) +{ + bool result = false; + const char java_method_name[] = "account_exists"; + const char java_method_signature[] = "(I[B)I"; + JNIEnv* jenv = attach(); + if (jenv != NULL) + { + jclass host_class; + jmethodID method; + jint jcontext_index; + jbyteArray jaddress; + + // get java class + host_class = (*jenv)->FindClass(jenv, "org/ethereum/evmc/Host"); + assert(host_class != NULL); + + // get java method + method = + (*jenv)->GetStaticMethodID(jenv, host_class, java_method_name, java_method_signature); + assert(method != NULL); + + // set java method params + assert(context != NULL); + jcontext_index = context->index; + + jaddress = (*jenv)->NewByteArray(jenv, sizeof(struct evmc_address)); + assert(jaddress != NULL); + (*jenv)->SetByteArrayRegion(jenv, jaddress, 0, sizeof(struct evmc_address), + (jbyte*)address); + + assert(jaddress != NULL); + + // call java method + jint jresult = + (*jenv)->CallStaticIntMethod(jenv, host_class, method, jcontext_index, jaddress); + result = !!jresult; + } + return result; +} + +static evmc_bytes32 get_storage_fn(struct evmc_host_context* context, + const evmc_address* address, + const evmc_bytes32* key) +{ + evmc_bytes32 result; + const char java_method_name[] = "get_storage"; + const char java_method_signature[] = "(I[B[B)Ljava/nio/ByteBuffer;"; + JNIEnv* jenv = attach(); + if (jenv != NULL) + { + jclass host_class; + jmethodID method; + jint jcontext_index; + jbyteArray jaddress; + jbyteArray jkey; + // get java class + host_class = (*jenv)->FindClass(jenv, "org/ethereum/evmc/Host"); + assert(host_class != NULL); + + // get java method + method = + (*jenv)->GetStaticMethodID(jenv, host_class, java_method_name, java_method_signature); + assert(method != NULL); + + // set java method params + + assert(context != NULL); + jcontext_index = context->index; + + jaddress = (*jenv)->NewByteArray(jenv, sizeof(struct evmc_address)); + assert(jaddress != NULL); + (*jenv)->SetByteArrayRegion(jenv, jaddress, 0, sizeof(struct evmc_address), + (jbyte*)address); + + jkey = (*jenv)->NewByteArray(jenv, sizeof(struct evmc_bytes32)); + assert(jkey != NULL); + (*jenv)->SetByteArrayRegion(jenv, jkey, 0, sizeof(struct evmc_bytes32), (jbyte*)key); + + // call java method + jobject jresult = (*jenv)->CallStaticObjectMethod(jenv, host_class, method, jcontext_index, + jaddress, jkey); + + assert(jresult != NULL); + evmc_bytes32* result_ptr = + (struct evmc_bytes32*)(*jenv)->GetDirectBufferAddress(jenv, jresult); + assert(result_ptr != NULL); + result = *result_ptr; + } + return result; +} + +static enum evmc_storage_status set_storage_fn(struct evmc_host_context* context, + const evmc_address* address, + const evmc_bytes32* key, + const evmc_bytes32* value) +{ + enum evmc_storage_status result; + const char java_method_name[] = "set_storage"; + const char java_method_signature[] = "(I[B[B[B)I"; + JNIEnv* jenv = attach(); + if (jenv != NULL) + { + jclass host_class; + jmethodID method; + jint jcontext_index; + jbyteArray jaddress; + jbyteArray jkey; + jbyteArray jvalue; + // get java class + host_class = (*jenv)->FindClass(jenv, "org/ethereum/evmc/Host"); + assert(host_class != NULL); + // get java method + method = + (*jenv)->GetStaticMethodID(jenv, host_class, java_method_name, java_method_signature); + assert(method != NULL); + + // set java method params + assert(context != NULL); + jcontext_index = context->index; + + jaddress = (*jenv)->NewByteArray(jenv, sizeof(struct evmc_address)); + assert(jaddress != NULL); + (*jenv)->SetByteArrayRegion(jenv, jaddress, 0, sizeof(struct evmc_address), + (jbyte*)address); + + jkey = (*jenv)->NewByteArray(jenv, sizeof(struct evmc_bytes32)); + assert(jkey != NULL); + (*jenv)->SetByteArrayRegion(jenv, jkey, 0, sizeof(struct evmc_bytes32), (jbyte*)key); + + jvalue = (*jenv)->NewByteArray(jenv, sizeof(struct evmc_bytes32)); + assert(jvalue != NULL); + (*jenv)->SetByteArrayRegion(jenv, jvalue, 0, sizeof(struct evmc_bytes32), (jbyte*)value); + + // call java method + jint jresult = (*jenv)->CallStaticIntMethod(jenv, host_class, method, jcontext_index, + jaddress, jkey, jvalue); + result = (enum evmc_storage_status)jresult; + } + return result; +} + +static evmc_uint256be get_balance_fn(struct evmc_host_context* context, const evmc_address* address) +{ + evmc_uint256be result; + char java_method_name[] = "get_balance"; + char java_method_signature[] = "(I[B)Ljava/nio/ByteBuffer;"; + JNIEnv* jenv = attach(); + if (jenv != NULL) + { + jclass host_class; + jmethodID method; + jint jcontext_index; + jbyteArray jaddress; + + // get java class + host_class = (*jenv)->FindClass(jenv, "org/ethereum/evmc/Host"); + assert(host_class != NULL); + + // get java method + method = + (*jenv)->GetStaticMethodID(jenv, host_class, java_method_name, java_method_signature); + assert(method != NULL); + + // set java method params + assert(context != NULL); + jcontext_index = context->index; + + jaddress = (*jenv)->NewByteArray(jenv, sizeof(struct evmc_address)); + assert(jaddress != NULL); + (*jenv)->SetByteArrayRegion(jenv, jaddress, 0, sizeof(struct evmc_address), + (jbyte*)address); + + // call java method + jobject jresult = + (*jenv)->CallStaticObjectMethod(jenv, host_class, method, jcontext_index, jaddress); + assert(jresult != NULL); + + evmc_uint256be* result_ptr = + (evmc_uint256be*)(*jenv)->GetDirectBufferAddress(jenv, jresult); + assert(result_ptr != NULL); + result = *result_ptr; + + (*jenv)->ReleaseByteArrayElements(jenv, jaddress, (jbyte*)address, 0); + } + return result; +} + +static size_t get_code_size_fn(struct evmc_host_context* context, const evmc_address* address) +{ + size_t result = 0; + char java_method_name[] = "get_code_size"; + char java_method_signature[] = "(I[B)I"; + JNIEnv* jenv = attach(); + if (jenv != NULL) + { + jclass host_class; + jmethodID method; + jint jcontext_index; + jbyteArray jaddress; + + // get java class + host_class = (*jenv)->FindClass(jenv, "org/ethereum/evmc/Host"); + assert(host_class != NULL); + + // get java method + method = + (*jenv)->GetStaticMethodID(jenv, host_class, java_method_name, java_method_signature); + assert(method != NULL); + + // set java method params + assert(context != NULL); + jcontext_index = context->index; + + jaddress = (*jenv)->NewByteArray(jenv, sizeof(struct evmc_address)); + assert(jaddress != NULL); + (*jenv)->SetByteArrayRegion(jenv, jaddress, 0, sizeof(struct evmc_address), + (jbyte*)address); + + // call java method + jint jresult = + (*jenv)->CallStaticIntMethod(jenv, host_class, method, jcontext_index, jaddress); + result = jresult; + } + return result; +} + +static evmc_bytes32 get_code_hash_fn(struct evmc_host_context* context, const evmc_address* address) +{ + evmc_bytes32 result; + char java_method_name[] = "get_code_hash"; + char java_method_signature[] = "(I[B)Ljava/nio/ByteBuffer;"; + JNIEnv* jenv = attach(); + if (jenv != NULL) + { + jclass host_class; + jmethodID method; + jint jcontext_index; + jbyteArray jaddress; + + // get java class + host_class = (*jenv)->FindClass(jenv, "org/ethereum/evmc/Host"); + assert(host_class != NULL); + // get java method + method = + (*jenv)->GetStaticMethodID(jenv, host_class, java_method_name, java_method_signature); + assert(method != NULL); + + // set java method params + + assert(context != NULL); + jcontext_index = context->index; + + jaddress = (*jenv)->NewByteArray(jenv, sizeof(struct evmc_address)); + assert(jaddress != NULL); + (*jenv)->SetByteArrayRegion(jenv, jaddress, 0, sizeof(struct evmc_address), + (jbyte*)address); + + // call java method + jobject jresult = + (*jenv)->CallStaticObjectMethod(jenv, host_class, method, jcontext_index, jaddress); + assert(jresult != NULL); + + evmc_bytes32* result_ptr = + (struct evmc_bytes32*)(*jenv)->GetDirectBufferAddress(jenv, jresult); + assert(result_ptr != NULL); + result = *result_ptr; + + (*jenv)->ReleaseByteArrayElements(jenv, jaddress, (jbyte*)address, 0); + } + return result; +} + +static size_t copy_code_fn(struct evmc_host_context* context, + const evmc_address* address, + size_t code_offset, + uint8_t* buffer_data, + size_t buffer_size) +{ + size_t result; + const char java_method_name[] = "copy_code"; + const char java_method_signature[] = "(I[BI)Ljava/nio/ByteBuffer;"; + JNIEnv* jenv = attach(); + if (jenv != NULL) + { + jclass host_class; + jmethodID method; + jint jcontext_index; + jbyteArray jaddress; + jint jcode_offset; + + // get java class + host_class = (*jenv)->FindClass(jenv, "org/ethereum/evmc/Host"); + assert(host_class != NULL); + + // get java method + method = + (*jenv)->GetStaticMethodID(jenv, host_class, java_method_name, java_method_signature); + assert(method != NULL); + + // set java method params + + assert(context != NULL); + jcontext_index = context->index; + + jaddress = (*jenv)->NewByteArray(jenv, sizeof(struct evmc_address)); + assert(jaddress != NULL); + (*jenv)->SetByteArrayRegion(jenv, jaddress, 0, sizeof(struct evmc_address), + (jbyte*)address); + + jcode_offset = code_offset; + + // call java method + jobject jresult = (*jenv)->CallStaticObjectMethod(jenv, host_class, method, jcontext_index, + jaddress, jcode_offset); + assert(jresult != NULL); + + // copy jresult back to buffer_data + buffer_data = (uint8_t*)(*jenv)->GetDirectBufferAddress(jenv, jresult); + assert(buffer_data != NULL); + + result = get_code_size_fn(context, address) - code_offset; + + (*jenv)->ReleaseByteArrayElements(jenv, jaddress, (jbyte*)address, 0); + } + return result; +} + +static void selfdestruct_fn(struct evmc_host_context* context, + const evmc_address* address, + const evmc_address* beneficiary) +{ + const char java_method_name[] = "selfdestruct"; + const char java_method_signature[] = "(I[B[B)V"; + JNIEnv* jenv = attach(); + if (jenv != NULL) + { + jclass host_class; + jmethodID method; + jint jcontext_index; + jbyteArray jaddress; + jbyteArray jbeneficiary; + + // get java class + host_class = (*jenv)->FindClass(jenv, "org/ethereum/evmc/Host"); + assert(host_class != NULL); + + // get java method + method = + (*jenv)->GetStaticMethodID(jenv, host_class, java_method_name, java_method_signature); + assert(method != NULL); + + // set java method params + assert(context != NULL); + jcontext_index = context->index; + + jaddress = (*jenv)->NewByteArray(jenv, sizeof(struct evmc_address)); + assert(jaddress != NULL); + (*jenv)->SetByteArrayRegion(jenv, jaddress, 0, sizeof(struct evmc_address), + (jbyte*)address); + + jbeneficiary = (*jenv)->NewByteArray(jenv, sizeof(struct evmc_address)); + assert(jbeneficiary != NULL); + (*jenv)->SetByteArrayRegion(jenv, jbeneficiary, 0, sizeof(struct evmc_address), + (jbyte*)beneficiary); + + // call java method + (*jenv)->CallStaticIntMethod(jenv, host_class, method, jcontext_index, jaddress, + jbeneficiary); + } + return; +} + +static struct evmc_result call_fn(struct evmc_host_context* context, const struct evmc_message* msg) +{ + struct evmc_result result; + const char java_method_name[] = "call"; + const char java_method_signature[] = "(ILjava/nio/ByteBuffer;)Ljava/nio/ByteBuffer;"; + JNIEnv* jenv = attach(); + if (jenv != NULL) + { + jclass host_class; + jmethodID method; + jint jcontext_index; + jobject jmsg; + + // get java class + host_class = (*jenv)->FindClass(jenv, "org/ethereum/evmc/Host"); + assert(host_class != NULL); + + // get java method + method = + (*jenv)->GetStaticMethodID(jenv, host_class, java_method_name, java_method_signature); + assert(method != NULL); + + // set java method params + assert(context != NULL); + jcontext_index = context->index; + + jmsg = (*jenv)->NewDirectByteBuffer(jenv, (void*)msg, sizeof(struct evmc_message)); + assert(jmsg != NULL); + + // call java method + jobject jresult = + (*jenv)->CallStaticObjectMethod(jenv, host_class, method, jcontext_index, jmsg); + assert(jresult != NULL); + + struct evmc_result* result_ptr = + (struct evmc_result*)(*jenv)->GetDirectBufferAddress(jenv, jresult); + assert(result_ptr != NULL); + result = *result_ptr; + } + return result; +} + +static struct evmc_tx_context get_tx_context_fn(struct evmc_host_context* context) +{ + struct evmc_tx_context result; + const char java_method_name[] = "get_tx_context"; + const char java_method_signature[] = "(I)Ljava/nio/ByteBuffer;"; + JNIEnv* jenv = attach(); + if (jenv != NULL) + { + jclass host_class; + jmethodID method; + jint jcontext_index; + // get java class + host_class = (*jenv)->FindClass(jenv, "org/ethereum/evmc/Host"); + assert(host_class != NULL); + + // get java method + method = + (*jenv)->GetStaticMethodID(jenv, host_class, java_method_name, java_method_signature); + assert(method != NULL); + + // set java method params + assert(context != NULL); + jcontext_index = context->index; + + // call java method + jobject jresult = (*jenv)->CallStaticObjectMethod(jenv, host_class, method, jcontext_index); + assert(jresult != NULL); + + struct evmc_tx_context* result_ptr = + (struct evmc_tx_context*)(*jenv)->GetDirectBufferAddress(jenv, jresult); + assert(result_ptr != NULL); + result = *result_ptr; + } + return result; +} + + +static evmc_bytes32 get_block_hash_fn(struct evmc_host_context* context, int64_t number) +{ + evmc_bytes32 result; + char java_method_name[] = "get_code_hash"; + char java_method_signature[] = "(IJ)Ljava/nio/ByteBuffer;"; + JNIEnv* jenv = attach(); + if (jenv != NULL) + { + jclass host_class; + jmethodID method; + jint jcontext_index; + jlong jnumber; + + // get java class + host_class = (*jenv)->FindClass(jenv, "org/ethereum/evmc/Host"); + assert(host_class != NULL); + + // get java method + method = + (*jenv)->GetStaticMethodID(jenv, host_class, java_method_name, java_method_signature); + assert(method != NULL); + + // set java method params + + assert(context != NULL); + jcontext_index = context->index; + + jnumber = (jlong)number; + + // call java method + jobject jresult = + (*jenv)->CallStaticObjectMethod(jenv, host_class, method, jcontext_index, jnumber); + assert(jresult != NULL); + + evmc_bytes32* result_ptr = + (struct evmc_bytes32*)(*jenv)->GetDirectBufferAddress(jenv, jresult); + assert(result_ptr != NULL); + result = *result_ptr; + } + return result; +} + +static void emit_log_fn(struct evmc_host_context* context, + const evmc_address* address, + const uint8_t* data, + size_t data_size, + const evmc_bytes32 topics[], + size_t topics_count) +{ + const char java_method_name[] = "emit_log"; + const char java_method_signature[] = "(I[B[BI[[BI)V"; + JNIEnv* jenv = attach(); + if (jenv != NULL) + { + jclass host_class; + jmethodID method; + jint jcontext_index; + jbyteArray jaddress; + jbyteArray jdata; + jobjectArray jtopics; + + // get java class + host_class = (*jenv)->FindClass(jenv, "org/ethereum/evmc/Host"); + assert(host_class != NULL); + + // get java method + method = + (*jenv)->GetStaticMethodID(jenv, host_class, java_method_name, java_method_signature); + assert(method != NULL); + + // set java method params + assert(context != NULL); + jcontext_index = context->index; + + jaddress = (*jenv)->NewByteArray(jenv, sizeof(struct evmc_address)); + assert(jaddress != NULL); + (*jenv)->SetByteArrayRegion(jenv, jaddress, 0, sizeof(struct evmc_address), + (jbyte*)address); + + jdata = (*jenv)->NewByteArray(jenv, data_size); + assert(jdata != NULL); + (*jenv)->SetByteArrayRegion(jenv, jdata, 0, data_size, (jbyte*)data); + + jclass byte_type = (*jenv)->FindClass(jenv, "[B"); + jtopics = (*jenv)->NewObjectArray(jenv, (jsize)topics_count, byte_type, NULL); + assert(jtopics != NULL); + for (int i = 0; i < topics_count; i++) + { + jbyteArray jtopic = (*jenv)->NewByteArray(jenv, sizeof(struct evmc_bytes32)); + assert(jtopic != NULL); + (*jenv)->SetByteArrayRegion(jenv, jtopic, 0, sizeof(struct evmc_bytes32), + (jbyte*)topics[i].bytes); + (*jenv)->SetObjectArrayElement(jenv, jtopics, i, jtopic); + (*jenv)->DeleteLocalRef(jenv, jtopic); + } + + // call java method + (*jenv)->CallStaticIntMethod(jenv, host_class, method, jcontext_index, jaddress, jdata, + data_size, jtopics, topics_count); + } + return; +} + +const struct evmc_host_interface* get_host_interface() +{ + static const struct evmc_host_interface host = { + account_exists_fn, get_storage_fn, set_storage_fn, get_balance_fn, + get_code_size_fn, get_code_hash_fn, copy_code_fn, selfdestruct_fn, + call_fn, get_tx_context_fn, get_block_hash_fn, emit_log_fn, + }; + return &host; +} diff --git a/bindings/java/c/host.h b/bindings/java/c/host.h new file mode 100644 index 000000000..041999df9 --- /dev/null +++ b/bindings/java/c/host.h @@ -0,0 +1,21 @@ +#include "evmc/evmc.h" +#include + +#ifndef _Included_org_ethereum_evmc_Host +#define _Included_org_ethereum_evmc_Host +#ifdef __cplusplus +extern "C" { +#endif + +struct evmc_host_context +{ + int index; +}; + +int set_jvm(JNIEnv*); +const struct evmc_host_interface* get_host_interface(); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/bindings/java/java/README.md b/bindings/java/java/README.md new file mode 100644 index 000000000..74b07c5e2 --- /dev/null +++ b/bindings/java/java/README.md @@ -0,0 +1,26 @@ +## Java Bindings + +This bindings have been tested with `openjdk64-11.0.1` on OSX and debian:latest. +For examples of how to use these bindings take a look at the JUnit tests. + +### Prerequisites + +You need to have [Gradle](https://www.gradle.org/installation) and [Java](https://www.oracle.com/technetwork/java/javase/downloads/index.html) installed. + +> Note: Requires Gradle 5.x + +### Build and test + +from project root: + +```bash +cd bindings/java && make clean build test +``` + +### Build and test (debug with debug printouts) + +from project root: + +```bash +cd bindings/java && make clean debug test +``` diff --git a/bindings/java/java/build.gradle b/bindings/java/java/build.gradle new file mode 100644 index 000000000..89fc23477 --- /dev/null +++ b/bindings/java/java/build.gradle @@ -0,0 +1,7 @@ +dependencies { + testImplementation 'org.apache.commons:commons-lang3:3.0' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.3.2' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.3.2' + + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.3.2' +} diff --git a/bindings/java/java/src/main/java/org/ethereum/evmc/EvmcVm.java b/bindings/java/java/src/main/java/org/ethereum/evmc/EvmcVm.java new file mode 100644 index 000000000..b4c8d52dc --- /dev/null +++ b/bindings/java/java/src/main/java/org/ethereum/evmc/EvmcVm.java @@ -0,0 +1,166 @@ +package org.ethereum.evmc; + +import static org.ethereum.evmc.Host.addContext; +import static org.ethereum.evmc.Host.removeContext; + +import java.nio.ByteBuffer; +import java.util.Objects; + +/** + * The Java interface to the evm instance. + * + *

Defines the Java methods capable of accessing the evm implementation. + */ +public final class EvmcVm implements AutoCloseable { + private static EvmcVm evmcVm; + private static boolean isEvmcLibraryLoaded = false; + private ByteBuffer nativeVm; + /** + * This method loads the specified evm shared library and loads/initializes the jni bindings. + * + * @param filename /path/filename of the evm shared object + */ + public static EvmcVm create(String filename) { + if (!EvmcVm.isEvmcLibraryLoaded) { + try { + // load so containing the jni bindings to evmc + System.load(System.getProperty("user.dir") + "/../c/build/evmc.so"); + EvmcVm.isEvmcLibraryLoaded = true; + } catch (UnsatisfiedLinkError e) { + System.err.println("Native code library failed to load.\n" + e); + System.exit(1); + } + } + if (Objects.isNull(evmcVm)) { + evmcVm = new EvmcVm(filename); + } + return evmcVm; + } + + private EvmcVm(String filename) { + // initialize jni and load EVM shared library + nativeVm = init(filename); + } + + /** + * This method loads the specified evm implementation and initializes jni + * + * @param filename path + filename of the evm shared object to load + * @return + */ + public native ByteBuffer init(String filename); + + /** + * EVMC ABI version implemented by the VM instance. + * + *

Can be used to detect ABI incompatibilities. The EVMC ABI version represented by this file + * is in ::EVMC_ABI_VERSION. + */ + public native int abi_version(); + + /** + * The name of the EVMC VM implementation. + * + *

It MUST be a NULL-terminated not empty string. The content MUST be UTF-8 encoded (this + * implies ASCII encoding is also allowed). + */ + native String name(ByteBuffer nativeVm); + + /** Function is a wrapper around native name(). */ + public String name() { + return name(nativeVm); + } + + /** + * The version of the EVMC VM implementation, e.g. "1.2.3b4". + * + *

It MUST be a NULL-terminated not empty string. The content MUST be UTF-8 encoded (this + * implies ASCII encoding is also allowed). + */ + native String version(ByteBuffer nativeVm); + + /** Function is a wrapper around native version(). */ + public String version() { + return version(nativeVm); + } + + /** + * Function to destroy the VM instance. + * + *

This is a mandatory method and MUST NOT be set to NULL. + */ + native void destroy(ByteBuffer nativeVm); + + /** + * Function to execute a code by the VM instance. + * + *

This is a mandatory method and MUST NOT be set to NULL. + */ + native void execute( + ByteBuffer nativeVm, + int context_index, + int rev, + ByteBuffer msg, + ByteBuffer code, + int size, + ByteBuffer result); + + /** + * Function is a wrapper around native execute. + * + *

This allows the context to managed in one method + */ + public synchronized ByteBuffer execute( + HostContext context, int rev, ByteBuffer msg, ByteBuffer code, int size) { + int context_index = addContext(context); + int resultSize = get_result_size(); + ByteBuffer result = ByteBuffer.allocateDirect(resultSize); + execute(nativeVm, context_index, rev, msg, code, size, result); + removeContext(context_index); + return result; + } + + /** + * A method returning capabilities supported by the VM instance. + * + *

The value returned MAY change when different options are set via the set_option() method. + * + *

A Client SHOULD only rely on the value returned if it has queried it after it has called the + * set_option(). + * + *

This is a mandatory method and MUST NOT be set to NULL. + */ + native int get_capabilities(ByteBuffer nativeVm); + + /** Function is a wrapper around native get_capabilities(). */ + public int get_capabilities() { + return get_capabilities(nativeVm); + } + + /** + * Function that modifies VM's options. + * + *

If the VM does not support this feature the pointer can be NULL. + */ + native int set_option(ByteBuffer nativeVm, String name, String value); + + /** Function is a wrapper around native set_option(). */ + public int set_option(String name, String value) { + return set_option(nativeVm, name, value); + } + + /** get size of result struct */ + native int get_result_size(); + + /** + * This method cleans up resources + * + * @throws Exception + */ + @Override + public void close() throws Exception { + destroy(nativeVm); + isEvmcLibraryLoaded = false; + evmcVm = null; + } +} diff --git a/bindings/java/java/src/main/java/org/ethereum/evmc/Host.java b/bindings/java/java/src/main/java/org/ethereum/evmc/Host.java new file mode 100644 index 000000000..209acfe63 --- /dev/null +++ b/bindings/java/java/src/main/java/org/ethereum/evmc/Host.java @@ -0,0 +1,150 @@ +package org.ethereum.evmc; + +import static java.util.Objects.requireNonNull; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * The Host interface. + * + *

The set of all callback functions expected by VM instances. + */ +final class Host { + /** Check account existence callback function. */ + static int account_exists(int context_index, byte[] address) { + HostContext context = + requireNonNull( + getContext(context_index), + "HostContext does not exist for context_index: " + context_index); + return context.accountExists(address) ? 1 : 0; + } + + /** Get storage callback function. */ + static ByteBuffer get_storage(int context_index, byte[] address, byte[] key) { + HostContext context = + requireNonNull( + getContext(context_index), + "HostContext does not exist for context_index: " + context_index); + return context.getStorage(address, key); + } + + /** Set storage callback function. */ + static int set_storage(int context_index, byte[] address, byte[] key, byte[] value) { + HostContext context = + requireNonNull( + getContext(context_index), + "HostContext does not exist for context_index: " + context_index); + return context.setStorage(address, key, value); + } + /** Get balance callback function. */ + static ByteBuffer get_balance(int context_index, byte[] address) { + HostContext context = + requireNonNull( + getContext(context_index), + "HostContext does not exist for context_index: " + context_index); + return context.getBalance(address); + } + + /** Get code size callback function. */ + static int get_code_size(int context_index, byte[] address) { + HostContext context = + requireNonNull( + getContext(context_index), + "HostContext does not exist for context_index: " + context_index); + return context.getCodeSize(address); + } + + /** Get code hash callback function. */ + static ByteBuffer get_code_hash(int context_index, byte[] address) { + HostContext context = + requireNonNull( + getContext(context_index), + "HostContext does not exist for context_index: " + context_index); + return context.getCodeHash(address); + } + + /** Copy code callback function. */ + static ByteBuffer copy_code(int context_index, byte[] address, int code_offset) { + HostContext context = + requireNonNull( + getContext(context_index), + "HostContext does not exist for context_index: " + context_index); + byte[] code = context.getCode(address).array(); + + if (code != null && code_offset > 0 && code_offset < code.length) { + int length = code.length - code_offset; + return ByteBuffer.wrap(code, 0, length); + } + + return ByteBuffer.wrap(new byte[0]); + } + + /** Selfdestruct callback function. */ + static void selfdestruct(int context_index, byte[] address, byte[] beneficiary) { + HostContext context = + requireNonNull( + getContext(context_index), + "HostContext does not exist for context_index: " + context_index); + context.selfdestruct(address, beneficiary); + } + + /** Call callback function. */ + static ByteBuffer call(int context_index, ByteBuffer msg) { + HostContext context = + requireNonNull( + getContext(context_index), + "HostContext does not exist for context_index: " + context_index); + return context.call(msg); + } + + /** Get transaction context callback function. */ + static ByteBuffer get_tx_context(int context_index) { + HostContext context = + requireNonNull( + getContext(context_index), + "HostContext does not exist for context_index: " + context_index); + return context.getTxContext(); + } + + /** Get block hash callback function. */ + static ByteBuffer get_block_hash_fn(int context_index, long number) { + HostContext context = + requireNonNull( + getContext(context_index), + "HostContext does not exist for context_index: " + context_index); + return context.getBlockHash(number); + } + + /** Emit log callback function. */ + static void emit_log( + int context_index, + byte[] address, + byte[] data, + int data_size, + byte[][] topics, + int topic_count) { + HostContext context = + requireNonNull( + getContext(context_index), + "HostContext does not exist for context_index: " + context_index); + context.emitLog(address, data, data_size, topics, topic_count); + } + + static HostContext getContext(int index) { + return contextList.get(index); + } + + static synchronized int addContext(HostContext context) { + contextList.add(context); + return contextList.size() - 1; + } + + static void removeContext(int index) { + contextList.remove(index); + } + + private static List contextList = Collections.synchronizedList(new ArrayList<>()); +} diff --git a/bindings/java/java/src/main/java/org/ethereum/evmc/HostContext.java b/bindings/java/java/src/main/java/org/ethereum/evmc/HostContext.java new file mode 100644 index 000000000..8a0747187 --- /dev/null +++ b/bindings/java/java/src/main/java/org/ethereum/evmc/HostContext.java @@ -0,0 +1,145 @@ +package org.ethereum.evmc; + +import java.nio.ByteBuffer; + +/** + * This interface represents the callback functions must be implemented in order to interface with + * the EVM. + */ +public interface HostContext { + /** + * Check account existence function. + * + *

This function is used by the VM to check if there exists an account at given address. + * + * @param address The address of the account the query is about. + * @return true if exists, false otherwise. + */ + boolean accountExists(byte[] address); + + /** + * Get storage function. + * + *

This function is used by a VM to query the given account storage entry. + * + * @param address The address of the account. + * @param key The index of the account's storage entry. + * @return The storage value at the given storage key or null bytes if the account does not exist. + */ + ByteBuffer getStorage(byte[] address, byte[] key); + + /** + * Set storage function. + * + *

This function is used by a VM to update the given account storage entry. The VM MUST make + * sure that the account exists. This requirement is only a formality because VM implementations + * only modify storage of the account of the current execution context (i.e. referenced by + * evmc_message::destination). + * + * @param address The address of the account. + * @param key The index of the storage entry. + * @param value The value to be stored. + * @return The effect on the storage item. + */ + int setStorage(byte[] address, byte[] key, byte[] value); + + /** + * Get balance function. + * + *

This function is used by a VM to query the balance of the given account. + * + * @param address The address of the account. + * @return The balance of the given account or 0 if the account does not exist. + */ + ByteBuffer getBalance(byte[] address); + + /** + * Get code size function. + * + *

This function is used by a VM to get the size of the code stored in the account at the given + * address. + * + * @param address The address of the account. + * @return The size of the code in the account or 0 if the account does not exist. + */ + int getCodeSize(byte[] address); + + /** + * Get code hash function. + * + *

This function is used by a VM to get the keccak256 hash of the code stored in the account at + * the given address. For existing accounts not having a code, this function returns keccak256 + * hash of empty data. + * + * @param address The address of the account. + * @return The hash of the code in the account or null bytes if the account does not exist. + */ + ByteBuffer getCodeHash(byte[] address); + + /** + * Copy code function. + * + *

This function is used by an EVM to request a copy of the code of the given account to the + * memory buffer provided by the EVM. The Client MUST copy the requested code, starting with the + * given offset, to the provided memory buffer up to the size of the buffer or the size of the + * code, whichever is smaller. + * + * @param address The address of the account. + * @return A copy of the requested code. + */ + ByteBuffer getCode(byte[] address); + + /** + * Selfdestruct function. + * + *

This function is used by an EVM to SELFDESTRUCT given contract. The execution of the + * contract will not be stopped, that is up to the EVM. + * + * @param address The address of the contract to be selfdestructed. + * @param beneficiary The address where the remaining ETH is going to be transferred. + */ + void selfdestruct(byte[] address, byte[] beneficiary); + + /** + * This function supports EVM calls. + * + * @param msg The call parameters. + * @return The result of the call. + */ + ByteBuffer call(ByteBuffer msg); + + /** + * Get transaction context function. + * + *

This function is used by an EVM to retrieve the transaction and block context. + * + * @return The transaction context. + */ + ByteBuffer getTxContext(); + + /** + * Get block hash function. + * + *

This function is used by a VM to query the hash of the header of the given block. If the + * information about the requested block is not available, then this is signalled by returning + * null bytes. + * + * @param number The block number. + * @return The block hash or null bytes if the information about the block is not available. + */ + ByteBuffer getBlockHash(long number); + + /** + * Log function. + * + *

This function is used by an EVM to inform about a LOG that happened during an EVM bytecode + * execution. + * + * @param address The address of the contract that generated the log. + * @param data The unindexed data attached to the log. + * @param dataSize The length of the data. + * @param topics The the array of topics attached to the log. + * @param topicCount The number of the topics. Valid values are between 0 and 4 inclusively. + */ + void emitLog(byte[] address, byte[] data, int dataSize, byte[][] topics, int topicCount); +} diff --git a/bindings/java/java/src/test/java/org/ethereum/evmc/EvmcTest.java b/bindings/java/java/src/test/java/org/ethereum/evmc/EvmcTest.java new file mode 100644 index 000000000..0dcdb3893 --- /dev/null +++ b/bindings/java/java/src/test/java/org/ethereum/evmc/EvmcTest.java @@ -0,0 +1,247 @@ +package org.ethereum.evmc; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +final class EvmcTest { + @Test + void testInitCloseDestroy() throws Exception { + Assertions.assertDoesNotThrow( + () -> { + try (EvmcVm vm = + EvmcVm.create(System.getProperty("user.dir") + "/../c/build/example_vm.so")) {} + }); + } + + @Test + void testAbiVersion() throws Exception { + try (EvmcVm vm = EvmcVm.create(System.getProperty("user.dir") + "/../c/build/example_vm.so")) { + int abiVersion = vm.abi_version(); + assert (abiVersion > 0); + } + } + + @Test + void testName() throws Exception { + try (EvmcVm vm = EvmcVm.create(System.getProperty("user.dir") + "/../c/build/example_vm.so")) { + String name = vm.name(); + assert (name.length() > 0); + assert (name.equals("example_vm")); + } + } + + @Test + void testVersion() throws Exception { + try (EvmcVm vm = EvmcVm.create(System.getProperty("user.dir") + "/../c/build/example_vm.so")) { + String version = vm.version(); + assert (version.length() > 0); + assert (version.equals("0.0.0")); + } + } + + @Test + void testExecute_returnAddress() throws Exception { + try (EvmcVm vm = EvmcVm.create(System.getProperty("user.dir") + "/../c/build/example_vm.so")) { + HostContext context = new TestHostContext(); + int BYZANTIUM = 4; + int EVMC_CALL = 0; + int kind = EVMC_CALL; + char[] sender = "39bf71de1b7d7be3b51\0".toCharArray(); + char[] destination = "53cf77204eEef952e25\0".toCharArray(); + char[] value = "1\0".toCharArray(); + char[] inputData = "hello w\0".toCharArray(); + long gas = 200000; + int depth = 0; + ByteBuffer msg = + new TestMessage(kind, sender, destination, value, inputData, gas, depth).toByteBuffer(); + + byte[] code = {0x30, 0x60, 0x00, 0x52, 0x59, 0x60, 0x00, (byte) 0xf3}; // return_address + ByteBuffer bbcode = ByteBuffer.allocateDirect(code.length).put(code); + + ByteBuffer result = + vm.execute(context, BYZANTIUM, msg, bbcode, code.length).order(ByteOrder.nativeOrder()); + int statusCode = result.getInt(); + result.getInt(); // padding + long gasLeft = result.getLong(); + assert (statusCode == 0); + assert (gasLeft == 0); + } + } + + /** Tests callbacks: get_storage_fn & set_storage_fn */ + @Test + void testExecute_counter() throws Exception { + try (EvmcVm vm = EvmcVm.create(System.getProperty("user.dir") + "/../c/build/example_vm.so")) { + HostContext context = new TestHostContext(); + int BYZANTIUM = 4; + int EVMC_CALL = 0; + int kind = EVMC_CALL; + char[] sender = "39bf71de1b7d7be3b51\0".toCharArray(); + char[] destination = "53cf77204eEef952e25\0".toCharArray(); + char[] value = "1\0".toCharArray(); + char[] inputData = "hello w\0".toCharArray(); + long gas = 200000; + int depth = 0; + ByteBuffer msg = + new TestMessage(kind, sender, destination, value, inputData, gas, depth).toByteBuffer(); + + byte[] code = {0x60, 0x01, 0x60, 0x00, 0x54, 0x01, 0x60, 0x00, 0x55}; // counter + ByteBuffer bbcode = ByteBuffer.allocateDirect(code.length).put(code); + + ByteBuffer result = + vm.execute(context, BYZANTIUM, msg, bbcode, code.length).order(ByteOrder.nativeOrder()); + int statusCode = result.getInt(); + result.getInt(); // padding + long gasLeft = result.getLong(); + assert (statusCode == 0); + assert (gasLeft == 0); + } + } + + /** Tests callbacks: get_tx_context_fn */ + @Test + void testExecute_returnBlockNumber() throws Exception { + try (EvmcVm vm = EvmcVm.create(System.getProperty("user.dir") + "/../c/build/example_vm.so")) { + HostContext context = new TestHostContext(); + int BYZANTIUM = 4; + int EVMC_CALL = 0; + int kind = EVMC_CALL; + char[] sender = "39bf71de1b7d7be3b51\0".toCharArray(); + char[] destination = "53cf77204eEef952e25\0".toCharArray(); + char[] value = "1\0".toCharArray(); + char[] inputData = "hello w\0".toCharArray(); + long gas = 200000; + int depth = 0; + ByteBuffer msg = + new TestMessage(kind, sender, destination, value, inputData, gas, depth).toByteBuffer(); + + byte[] code = {0x43, 0x60, 0x00, 0x52, 0x59, 0x60, 0x00, (byte) 0xf3}; // return_block_number( + ByteBuffer bbcode = ByteBuffer.allocateDirect(code.length).put(code); + + ByteBuffer result = + vm.execute(context, BYZANTIUM, msg, bbcode, code.length).order(ByteOrder.nativeOrder()); + int statusCode = result.getInt(); + result.getInt(); // padding + long gasLeft = result.getLong(); + assert (statusCode == 0); + assert (gasLeft == gas / 2); + } + } + + /** Tests callbacks: get_tx_context_fn & set_storage_fn */ + @Test + void testExecute_saveReturnBlockNumber() throws Exception { + try (EvmcVm vm = EvmcVm.create(System.getProperty("user.dir") + "/../c/build/example_vm.so")) { + HostContext context = new TestHostContext(); + int BYZANTIUM = 4; + int EVMC_CALL = 0; + int kind = EVMC_CALL; + char[] sender = "39bf71de1b7d7be3b51\0".toCharArray(); + char[] destination = "53cf77204eEef952e25\0".toCharArray(); + char[] value = "1\0".toCharArray(); + char[] inputData = "hello w\0".toCharArray(); + long gas = 200000; + int depth = 0; + ByteBuffer msg = + new TestMessage(kind, sender, destination, value, inputData, gas, depth).toByteBuffer(); + + byte[] code = { + 0x43, 0x60, 0x00, 0x55, 0x43, 0x60, 0x00, 0x52, 0x59, 0x60, 0x00, (byte) 0xf3 + }; // save_return_block_number + ByteBuffer bbcode = ByteBuffer.allocateDirect(code.length).put(code); + + ByteBuffer result = + vm.execute(context, BYZANTIUM, msg, bbcode, code.length).order(ByteOrder.nativeOrder()); + int statusCode = result.getInt(); + result.getInt(); // padding + long gasLeft = result.getLong(); + assert (statusCode == 0); + assert (gasLeft == gas / 2); + } + } + + /** Tests callbacks: call_fn */ + @Test + void testExecute_makeCall() throws Exception { + try (EvmcVm vm = EvmcVm.create(System.getProperty("user.dir") + "/../c/build/example_vm.so")) { + HostContext context = new TestHostContext(); + int BYZANTIUM = 4; + int EVMC_CALL = 0; + int kind = EVMC_CALL; + char[] sender = "39bf71de1b7d7be3b51\0".toCharArray(); + char[] destination = "53cf77204eEef952e25\0".toCharArray(); + char[] value = "1\0".toCharArray(); + char[] inputData = "hello w\0".toCharArray(); + long gas = 200000; + int depth = 0; + ByteBuffer msg = + new TestMessage(kind, sender, destination, value, inputData, gas, depth).toByteBuffer(); + byte[] code = { + 0x60, + 0x00, + (byte) 0x80, + (byte) 0x80, + (byte) 0x80, + (byte) 0x80, + (byte) 0x80, + (byte) 0x80, + (byte) 0xf1 + }; // make_a_call( + ByteBuffer bbcode = ByteBuffer.allocateDirect(code.length).put(code); + + ByteBuffer result = + vm.execute(context, BYZANTIUM, msg, bbcode, code.length).order(ByteOrder.nativeOrder()); + int statusCode = result.getInt(); + result.getInt(); // padding + long gasLeft = result.getLong(); + assert (statusCode == 0); + assert (gasLeft == 0); // gas - gas / 64); + } + } + + @Test + void testExecute_EVMC_CREATE() throws Exception { + try (EvmcVm vm = EvmcVm.create(System.getProperty("user.dir") + "/../c/build/example_vm.so")) { + HostContext context = new TestHostContext(); + int BYZANTIUM = 4; + int EVMC_CREATE = 3; + int kind = EVMC_CREATE; + char[] sender = "39bf71de1b7d7be3b51\\0".toCharArray(); + char[] destination = "53cf77204eEef952e25\0".toCharArray(); + char[] value = "1\0".toCharArray(); + char[] inputData = "hello w\0".toCharArray(); + long gas = 200000; + int depth = 0; + ByteBuffer msg = + new TestMessage(kind, sender, destination, value, inputData, gas, depth).toByteBuffer(); + byte[] code = {0x00}; + ByteBuffer bbcode = ByteBuffer.allocateDirect(code.length).put(code); + + ByteBuffer result = + vm.execute(context, BYZANTIUM, msg, bbcode, code.length).order(ByteOrder.nativeOrder()); + int statusCode = result.getInt(); + result.getInt(); // padding + long gasLeft = result.getLong(); + assert (statusCode == 0); + assert (gasLeft == gas / 10); + } + } + + @Test + void testGetCapabilities() throws Exception { + try (EvmcVm vm = EvmcVm.create(System.getProperty("user.dir") + "/../c/build/example_vm.so")) { + int capabilities = vm.get_capabilities(); + assert (capabilities > 0); + } + } + + @Test + void testSetOption() throws Exception { + try (EvmcVm vm = EvmcVm.create(System.getProperty("user.dir") + "/../c/build/example_vm.so")) { + int result = vm.set_option("verbose", "1"); + assert (result == 0); + } + } +} diff --git a/bindings/java/java/src/test/java/org/ethereum/evmc/TestHostContext.java b/bindings/java/java/src/test/java/org/ethereum/evmc/TestHostContext.java new file mode 100644 index 000000000..e4cbb87ee --- /dev/null +++ b/bindings/java/java/src/test/java/org/ethereum/evmc/TestHostContext.java @@ -0,0 +1,61 @@ +package org.ethereum.evmc; + +import java.nio.ByteBuffer; + +class TestHostContext implements HostContext { + @Override + public boolean accountExists(byte[] address) { + return true; + } + + @Override + public ByteBuffer getStorage(byte[] address, byte[] key) { + return ByteBuffer.allocateDirect(64).put(new byte[64]); + } + + @Override + public int setStorage(byte[] address, byte[] key, byte[] value) { + return 0; + } + + @Override + public ByteBuffer getBalance(byte[] address) { + return ByteBuffer.allocateDirect(64).put(new byte[64]); + } + + @Override + public int getCodeSize(byte[] address) { + return address.length; + } + + @Override + public ByteBuffer getCodeHash(byte[] address) { + return ByteBuffer.allocateDirect(64).put(new byte[64]); + } + + @Override + public ByteBuffer getCode(byte[] address) { + return ByteBuffer.allocateDirect(64).put(new byte[64]); + } + + @Override + public void selfdestruct(byte[] address, byte[] beneficiary) {} + + @Override + public ByteBuffer call(ByteBuffer msg) { + return ByteBuffer.allocateDirect(64).put(new byte[64]); + } + + @Override + public ByteBuffer getTxContext() { + return ByteBuffer.allocateDirect(64).put(new byte[64]); + } + + @Override + public ByteBuffer getBlockHash(long number) { + return ByteBuffer.allocateDirect(64).put(new byte[64]); + } + + @Override + public void emitLog(byte[] address, byte[] data, int dataSize, byte[][] topics, int topicCount) {} +} diff --git a/bindings/java/java/src/test/java/org/ethereum/evmc/TestMessage.java b/bindings/java/java/src/test/java/org/ethereum/evmc/TestMessage.java new file mode 100644 index 000000000..faee0cba7 --- /dev/null +++ b/bindings/java/java/src/test/java/org/ethereum/evmc/TestMessage.java @@ -0,0 +1,74 @@ +package org.ethereum.evmc; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.CharBuffer; +import java.nio.charset.StandardCharsets; + +public class TestMessage { + int kind; + int flags; + int depth; + long gas; + char[] destination; + char[] sender; + char[] inputData; + long inputSize; + char[] value; + byte[] createSalt; + + public TestMessage( + int kind, + char[] sender, + char[] destination, + char[] value, + char[] inputData, + long gas, + int depth) { + this.kind = kind; + this.flags = 0; + this.depth = depth; + this.gas = gas; + this.destination = destination; + this.sender = sender; + this.inputData = inputData; + this.inputSize = (long) inputData.length; + this.value = value; + this.createSalt = new byte[32]; + } + + public TestMessage(ByteBuffer msg) { + this.kind = msg.getInt(); + this.flags = msg.getInt(); + this.depth = msg.getInt(); + msg.getInt(); // padding + this.gas = msg.getLong(); + ByteBuffer tmpbuf = msg.get(new byte[20]); + this.destination = StandardCharsets.ISO_8859_1.decode(tmpbuf).array(); + tmpbuf = msg.get(new byte[20]); + this.sender = StandardCharsets.ISO_8859_1.decode(tmpbuf).array(); + tmpbuf = msg.get(new byte[8]); + this.inputData = StandardCharsets.ISO_8859_1.decode(tmpbuf).array(); + this.inputSize = msg.getLong(); + tmpbuf = msg.get(new byte[32]); + this.value = StandardCharsets.ISO_8859_1.decode(tmpbuf).array(); + this.createSalt = msg.get(new byte[32]).array(); + } + + public ByteBuffer toByteBuffer() { + + return ByteBuffer.allocateDirect(152) + .order(ByteOrder.nativeOrder()) + .putInt(kind) // 4 + .putInt(flags) // 4 + .putInt(depth) // 4 + .put(new byte[4]) // 4 (padding) + .putLong(gas) // 8 + .put(StandardCharsets.ISO_8859_1.encode(CharBuffer.wrap(destination))) // 20 + .put(StandardCharsets.ISO_8859_1.encode(CharBuffer.wrap(sender))) // 20 + .put(StandardCharsets.ISO_8859_1.encode(CharBuffer.wrap(inputData))) // 8 + .putLong(inputSize) // 8 + .put(StandardCharsets.ISO_8859_1.encode(CharBuffer.wrap(value))) // 32 + .put(createSalt); // 32 + } +} diff --git a/bindings/java/settings.gradle b/bindings/java/settings.gradle new file mode 100644 index 000000000..395ce3e0a --- /dev/null +++ b/bindings/java/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'evmc' +include 'java' diff --git a/bindings/java/wrapper.gradle b/bindings/java/wrapper.gradle new file mode 100644 index 000000000..3dd555611 --- /dev/null +++ b/bindings/java/wrapper.gradle @@ -0,0 +1,4 @@ +task setup(type: Wrapper) { + description = "Download the gradle wrapper and requisite files. Overwrites existing wrapper files." + gradleVersion = "5.0" +} diff --git a/circle.yml b/circle.yml index 1f6a48fb9..fad610790 100644 --- a/circle.yml +++ b/circle.yml @@ -64,7 +64,7 @@ jobs: name: "Check code format" command: | clang-format --version - find examples include lib test -name '*.hpp' -o -name '*.cpp' -o -name '*.h' -o -name '*.c' | xargs clang-format -i + find bindings/java examples include lib test -name '*.hpp' -o -name '*.cpp' -o -name '*.h' -o -name '*.c' | xargs clang-format -i git diff --color --exit-code - run: name: "Run codespell" @@ -182,6 +182,25 @@ jobs: - image: circleci/golang:1.9 steps: *bindings-go-steps + bindings-java: + docker: + - image: circleci/openjdk:11-stretch + steps: + - checkout + - run: + name: Update environment + command: | + sudo apt -qq update + sudo apt -yq install build-essential + - run: + name: "Java Build" + command: | + cd bindings/java && make clean build + - run: + name: "Java Test" + command: | + cd bindings/java && make test + bindings-rust: docker: - image: rust:1 @@ -261,6 +280,7 @@ workflows: - build-gcc-32bit - bindings-go-latest - bindings-go-min + - bindings-java - bindings-rust: requires: - build-gcc8-cxx17