diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2aaea06 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/Java/bin/ +/Java/target/ +/Java/.settings/ diff --git a/Java/.classpath b/Java/.classpath new file mode 100644 index 0000000..f3d3455 --- /dev/null +++ b/Java/.classpath @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Java/.project b/Java/.project new file mode 100644 index 0000000..70a0c8c --- /dev/null +++ b/Java/.project @@ -0,0 +1,23 @@ + + + CIYAM-AT-Java + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.jdt.core.javanature + + diff --git a/Java/pom.xml b/Java/pom.xml new file mode 100644 index 0000000..96a3131 --- /dev/null +++ b/Java/pom.xml @@ -0,0 +1,28 @@ + + 4.0.0 + CIYAM-AT-Java + CIYAM-AT-Java + 1.0 + + src + tests + + + maven-compiler-plugin + 3.5.1 + + 1.8 + 1.8 + + + + + + + org.bouncycastle + bcprov-jdk15on + 1.60 + test + + + \ No newline at end of file diff --git a/Java/src/org/ciyam/at/API.java b/Java/src/org/ciyam/at/API.java new file mode 100644 index 0000000..c012e27 --- /dev/null +++ b/Java/src/org/ciyam/at/API.java @@ -0,0 +1,100 @@ +package org.ciyam.at; + +/** + * API for CIYAM AT "Function Codes" for blockchain-specific interactions. + *

+ * For more information, see the specification document at:
+ * Automated Transactions API Specification + *

+ * Note that "timestamp" does not mean a real timestamp but instead is an artificial timestamp that includes two parts. The first part is a block height (32 + * bits) with the second part being the number of the transaction if applicable (also 32 bits and zero if not applicable). + * + */ +public interface API { + + /** Returns current blockchain's height */ + public int getCurrentBlockHeight(); + + /** Returns block height where AT was created */ + public int getATCreationBlockHeight(MachineState state); + + /** Returns previous block's height */ + default public int getPreviousBlockHeight() { + return getCurrentBlockHeight() - 1; + } + + /** Put previous block's signature hash in A */ + public void putPreviousBlockHashInA(MachineState state); + + /** Put next transaction to AT after timestamp in A */ + public void putTransactionAfterTimestampInA(Timestamp timestamp, MachineState state); + + /** Return type from transaction in A, or 0xffffffffffffffff if A not valid transaction */ + public long getTypeFromTransactionInA(MachineState state); + + /** Return amount from transaction in A, after transaction fees have been deducted, or 0xffffffffffffffff if A not valid transaction */ + public long getAmountFromTransactionInA(MachineState state); + + /** Return timestamp from transaction in A, or 0xffffffffffffffff if A not valid transaction */ + public long getTimestampFromTransactionInA(MachineState state); + + /** + * Generate pseudo-random number using transaction in A. + *

+ * AT should sleep so it can use next block as source of entropy. + *

+ * Set state.isSleeping = true before exit on first call.
+ * state.steps will be zero on second call after wake-up. + *

+ * Returns 0xffffffffffffffff if A not valid transaction. + */ + public long generateRandomUsingTransactionInA(MachineState state); + + /** Put 'message' from transaction in A into B */ + public void putMessageFromTransactionInAIntoB(MachineState state); + + /** Put sender/creator address from transaction in A into B */ + public void putAddressFromTransactionInAIntoB(MachineState state); + + /** Put AT's creator's address into B */ + public void putCreatorAddressIntoB(MachineState state); + + /** Return AT's current balance */ + public long getCurrentBalance(MachineState state); + + /** Return AT's previous balance at end of last execution round. Does not include any amounts sent to AT since */ + public long getPreviousBalance(MachineState state); + + /** Pay passed amount, or current balance if necessary, (fee inclusive) to address in B */ + public void payAmountToB(long value1, MachineState state); + + /** Pay AT's current balance to address in B */ + public void payCurrentBalanceToB(MachineState state); + + /** Pay AT's previous balance to address in B */ + public void payPreviousBalanceToB(MachineState state); + + /** Send 'message' in A to address in B */ + public void messageAToB(MachineState state); + + /** + * Returns minutes of blocks added to 'timestamp' + *

+ * minutes is converted to rough number of blocks and added to 'timestamp' to create return value. + */ + public long addMinutesToTimestamp(Timestamp timestamp, long minutes, MachineState state); + + /** AT has encountered fatal error. Return remaining funds to creator */ + public void onFatalError(MachineState state, ExecutionException e); + + /** Pre-execute checking of param requirements for platform-specific functions */ + public void platformSpecificPreExecuteCheck(short functionCodeValue, int paramCount, boolean returnValueExpected) throws IllegalFunctionCodeException; + + /** + * Platform-specific function execution + * + * @throws ExecutionException + */ + public void platformSpecificPostCheckExecute(short functionCodeValue, FunctionData functionData, MachineState state) throws ExecutionException; + +} diff --git a/Java/src/org/ciyam/at/CodeSegmentException.java b/Java/src/org/ciyam/at/CodeSegmentException.java new file mode 100644 index 0000000..481fdd2 --- /dev/null +++ b/Java/src/org/ciyam/at/CodeSegmentException.java @@ -0,0 +1,21 @@ +package org.ciyam.at; + +@SuppressWarnings("serial") +public class CodeSegmentException extends ExecutionException { + + public CodeSegmentException() { + } + + public CodeSegmentException(String message) { + super(message); + } + + public CodeSegmentException(Throwable cause) { + super(cause); + } + + public CodeSegmentException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/Java/src/org/ciyam/at/ExecutionException.java b/Java/src/org/ciyam/at/ExecutionException.java new file mode 100644 index 0000000..43e1daf --- /dev/null +++ b/Java/src/org/ciyam/at/ExecutionException.java @@ -0,0 +1,21 @@ +package org.ciyam.at; + +@SuppressWarnings("serial") +public class ExecutionException extends Exception { + + public ExecutionException() { + } + + public ExecutionException(String message) { + super(message); + } + + public ExecutionException(Throwable cause) { + super(cause); + } + + public ExecutionException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/Java/src/org/ciyam/at/FunctionCode.java b/Java/src/org/ciyam/at/FunctionCode.java new file mode 100644 index 0000000..1aa3aeb --- /dev/null +++ b/Java/src/org/ciyam/at/FunctionCode.java @@ -0,0 +1,956 @@ +package org.ciyam.at; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * This enum contains function codes for the CIYAM AT machine. + *

+ * Function codes are represented by a short. Functions can take 0 to 2 additional long values and optionally return a value too. + *

+ * FunctionCode instances can be obtained via the default FunctionCode.valueOf(String) or the additional FunctionCode.valueOf(int). + *

+ * Use the FunctionCode.execute method to perform the operation. + *

+ * For more details, view the API Specification. + * + * @see FunctionCode#valueOf(int) + * @see FunctionCode#execute(FunctionData, MachineState) + */ +public enum FunctionCode { + /** + * ECHO value to logger
+ * 0x0001 value + */ + ECHO(0x0001, 1, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + String message = String.valueOf(functionData.value1); + state.logger.echo(message); + } + }, + /** + * 0x0100
+ * Returns A1 value + */ + GET_A1(0x0100, 0, true) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + functionData.returnValue = state.a1; + } + }, + /** + * 0x0101
+ * Returns A2 value + */ + GET_A2(0x0101, 0, true) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + functionData.returnValue = state.a2; + } + }, + /** + * 0x0102
+ * Returns A3 value + */ + GET_A3(0x0102, 0, true) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + functionData.returnValue = state.a3; + } + }, + /** + * 0x0103
+ * Returns A4 value + */ + GET_A4(0x0103, 0, true) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + functionData.returnValue = state.a4; + } + }, + /** + * 0x0104
+ * Returns B1 value + */ + GET_B1(0x0104, 0, true) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + functionData.returnValue = state.b1; + } + }, + /** + * 0x0105
+ * Returns B2 value + */ + GET_B2(0x0105, 0, true) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + functionData.returnValue = state.b2; + } + }, + /** + * 0x0106
+ * Returns B3 value + */ + GET_B3(0x0106, 0, true) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + functionData.returnValue = state.b3; + } + }, + /** + * 0x0107
+ * Returns B4 value + */ + GET_B4(0x0107, 0, true) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + functionData.returnValue = state.b4; + } + }, + /** + * Set A1
+ * 0x0110 value + */ + SET_A1(0x0110, 1, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + state.a1 = functionData.value1; + } + }, + /** + * Set A2
+ * 0x0111 value + */ + SET_A2(0x0111, 1, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + state.a2 = functionData.value1; + } + }, + /** + * Set A3
+ * 0x0112 value + */ + SET_A3(0x0112, 1, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + state.a3 = functionData.value1; + } + }, + /** + * Set A4
+ * 0x0113 value + */ + SET_A4(0x0113, 1, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + state.a4 = functionData.value1; + } + }, + /** + * Set A1 and A2
+ * 0x0114 value value + */ + SET_A1_A2(0x0114, 2, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + state.a1 = functionData.value1; + state.a2 = functionData.value2; + } + }, + /** + * Set A3 and A4
+ * 0x0115 value value + */ + SET_A3_A4(0x0115, 2, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + state.a3 = functionData.value1; + state.a4 = functionData.value2; + } + }, + /** + * Set B1
+ * 0x0116 value + */ + SET_B1(0x0116, 1, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + state.b1 = functionData.value1; + } + }, + /** + * Set B2
+ * 0x0117 value + */ + SET_B2(0x0117, 1, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + state.b2 = functionData.value1; + } + }, + /** + * Set B3
+ * 0x0118 value + */ + SET_B3(0x0118, 1, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + state.b3 = functionData.value1; + } + }, + /** + * Set B4
+ * 0x0119 value + */ + SET_B4(0x0119, 1, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + state.b4 = functionData.value1; + } + }, + /** + * Set B1 and B2
+ * 0x011a value value + */ + SET_B1_B2(0x011a, 2, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + state.b1 = functionData.value1; + state.b2 = functionData.value2; + } + }, + /** + * Set B3 and B4
+ * 0x011b value value + */ + SET_B3_B4(0x011b, 2, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + state.b3 = functionData.value1; + state.b4 = functionData.value2; + } + }, + /** + * Clear A
+ * 0x0120 + */ + CLEAR_A(0x0120, 0, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + state.a1 = 0; + state.a2 = 0; + state.a3 = 0; + state.a4 = 0; + } + }, + /** + * Clear B
+ * 0x0121 + */ + CLEAR_B(0x0121, 0, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + state.b1 = 0; + state.b2 = 0; + state.b3 = 0; + state.b4 = 0; + } + }, + /** + * Clear A and B
+ * 0x0122 + */ + CLEAR_A_AND_B(0x0122, 0, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + state.a1 = 0; + state.a2 = 0; + state.a3 = 0; + state.a4 = 0; + state.b1 = 0; + state.b2 = 0; + state.b3 = 0; + state.b4 = 0; + } + }, + /** + * Copy A from B
+ * 0x0123 + */ + COPY_A_FROM_B(0x0123, 0, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + state.a1 = state.b1; + state.a2 = state.b2; + state.a3 = state.b3; + state.a4 = state.b4; + } + }, + /** + * Copy B from A
+ * 0x0124 + */ + COPY_B_FROM_A(0x0124, 0, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + state.b1 = state.a1; + state.b2 = state.a2; + state.b3 = state.a3; + state.b4 = state.a4; + } + }, + /** + * Check A is zero
+ * 0x0125
+ * Returns 1 if true, 0 if false + */ + CHECK_A_IS_ZERO(0x0125, 0, true) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + if (state.a1 == 0 && state.a2 == 0 && state.a3 == 0 && state.a4 == 0) + functionData.returnValue = 1L; // true + else + functionData.returnValue = 0L; // false + } + }, + /** + * Check B is zero
+ * 0x0126
+ * Returns 1 if true, 0 if false + */ + CHECK_B_IS_ZERO(0x0126, 0, true) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + if (state.b1 == 0 && state.b2 == 0 && state.b3 == 0 && state.b4 == 0) + functionData.returnValue = 1L; // true + else + functionData.returnValue = 0L; // false + } + }, + /** + * Check A equals B
+ * 0x0127
+ * Returns 1 if true, 0 if false + */ + CHECK_A_EQUALS_B(0x0127, 0, true) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + if (state.a1 == state.b1 && state.a2 == state.b2 && state.a3 == state.b3 && state.a4 == state.b4) + functionData.returnValue = 1L; // true + else + functionData.returnValue = 0L; // false + } + }, + /** + * Swap A with B
+ * 0x0128 + */ + SWAP_A_AND_B(0x0128, 0, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + long tmp1 = state.a1; + long tmp2 = state.a2; + long tmp3 = state.a3; + long tmp4 = state.a4; + + state.a1 = state.b1; + state.a2 = state.b2; + state.a3 = state.b3; + state.a4 = state.b4; + + state.b1 = tmp1; + state.b2 = tmp2; + state.b3 = tmp3; + state.b4 = tmp4; + } + }, + /** + * Bitwise-OR A with B
+ * 0x0129 + */ + OR_A_WITH_B(0x0129, 0, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + state.a1 = state.a1 | state.b1; + state.a2 = state.a2 | state.b2; + state.a3 = state.a3 | state.b3; + state.a4 = state.a4 | state.b4; + } + }, + /** + * Bitwise-OR B with A
+ * 0x012a + */ + OR_B_WITH_A(0x012a, 0, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + state.b1 = state.a1 | state.b1; + state.b2 = state.a2 | state.b2; + state.b3 = state.a3 | state.b3; + state.b4 = state.a4 | state.b4; + } + }, + /** + * Bitwise-AND A with B
+ * 0x012b + */ + AND_A_WITH_B(0x012b, 0, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + state.a1 = state.a1 & state.b1; + state.a2 = state.a2 & state.b2; + state.a3 = state.a3 & state.b3; + state.a4 = state.a4 & state.b4; + } + }, + /** + * Bitwise-AND B with A
+ * 0x012c + */ + AND_B_WITH_A(0x012c, 0, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + state.b1 = state.a1 & state.b1; + state.b2 = state.a2 & state.b2; + state.b3 = state.a3 & state.b3; + state.b4 = state.a4 & state.b4; + } + }, + /** + * Bitwise-XOR A with B
+ * 0x012d + */ + XOR_A_WITH_B(0x012d, 0, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + state.a1 = state.a1 ^ state.b1; + state.a2 = state.a2 ^ state.b2; + state.a3 = state.a3 ^ state.b3; + state.a4 = state.a4 ^ state.b4; + } + }, + /** + * Bitwise-XOR B with A
+ * 0x012e + */ + XOR_B_WITH_A(0x012e, 0, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + state.b1 = state.a1 ^ state.b1; + state.b2 = state.a2 ^ state.b2; + state.b3 = state.a3 ^ state.b3; + state.b4 = state.a4 ^ state.b4; + } + }, + /** + * MD5 A into B
+ * 0x0200 + */ + MD5_A_TO_B(0x0200, 0, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + ByteBuffer messageByteBuffer = ByteBuffer.allocate(2 * MachineState.VALUE_SIZE); + messageByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + + messageByteBuffer.putLong(state.a1); + messageByteBuffer.putLong(state.a2); + + byte[] message = messageByteBuffer.array(); + + try { + MessageDigest digester = MessageDigest.getInstance("MD5"); + byte[] digest = digester.digest(message); + + ByteBuffer digestByteBuffer = ByteBuffer.wrap(digest); + digestByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + + state.b1 = digestByteBuffer.getLong(); + state.b2 = digestByteBuffer.getLong(); + state.b3 = 0L; // XXX Or do we leave B3 untouched? + state.b4 = 0L; // XXX Or do we leave B4 untouched? + } catch (NoSuchAlgorithmException e) { + throw new ExecutionException("No MD5 message digest service available", e); + } + } + }, + /** + * Check MD5 of A matches B
+ * 0x0201
+ * Returns 1 if true, 0 if false + */ + CHECK_MD5_A_WITH_B(0x0201, 0, true) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + ByteBuffer messageByteBuffer = ByteBuffer.allocate(2 * MachineState.VALUE_SIZE); + messageByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + + messageByteBuffer.putLong(state.a1); + messageByteBuffer.putLong(state.a2); + + byte[] message = messageByteBuffer.array(); + + try { + MessageDigest digester = MessageDigest.getInstance("MD5"); + byte[] actualDigest = digester.digest(message); + + ByteBuffer digestByteBuffer = ByteBuffer.allocate(2 * MachineState.VALUE_SIZE); + digestByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + + digestByteBuffer.putLong(state.b1); + digestByteBuffer.putLong(state.b2); + + byte[] expectedDigest = digestByteBuffer.array(); + + if (Arrays.equals(actualDigest, expectedDigest)) + functionData.returnValue = 1L; // true + else + functionData.returnValue = 0L; // false + } catch (NoSuchAlgorithmException e) { + throw new ExecutionException("No MD5 message digest service available", e); + } + } + }, + /** + * HASH160 A into B
+ * 0x0202 + */ + HASH160_A_TO_B(0x0202, 0, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + ByteBuffer messageByteBuffer = ByteBuffer.allocate(3 * MachineState.VALUE_SIZE); + messageByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + + messageByteBuffer.putLong(state.a1); + messageByteBuffer.putLong(state.a2); + messageByteBuffer.putLong(state.a3); + + byte[] message = messageByteBuffer.array(); + + try { + MessageDigest digester = MessageDigest.getInstance("RIPEMD160"); + byte[] digest = digester.digest(message); + + ByteBuffer digestByteBuffer = ByteBuffer.wrap(digest); + digestByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + + state.b1 = digestByteBuffer.getLong(); + state.b2 = digestByteBuffer.getLong(); + state.b3 = (long) digestByteBuffer.getInt() & 0xffffffffL; + state.b4 = 0L; // XXX Or do we leave B4 untouched? + } catch (NoSuchAlgorithmException e) { + throw new ExecutionException("No RIPEMD160 message digest service available", e); + } + } + }, + /** + * Check HASH160 of A matches B
+ * 0x0203
+ * Returns 1 if true, 0 if false + */ + CHECK_HASH160_A_WITH_B(0x0203, 0, true) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + ByteBuffer messageByteBuffer = ByteBuffer.allocate(3 * MachineState.VALUE_SIZE); + messageByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + + messageByteBuffer.putLong(state.a1); + messageByteBuffer.putLong(state.a2); + messageByteBuffer.putLong(state.a3); + + byte[] message = messageByteBuffer.array(); + + try { + MessageDigest digester = MessageDigest.getInstance("RIPEMD160"); + byte[] actualDigest = digester.digest(message); + + ByteBuffer digestByteBuffer = ByteBuffer.allocate(digester.getDigestLength()); + digestByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + + digestByteBuffer.putLong(state.b1); + digestByteBuffer.putLong(state.b2); + digestByteBuffer.putInt((int) (state.b3 & 0xffffffffL)); + // XXX: b4 ignored + + byte[] expectedDigest = digestByteBuffer.array(); + + if (Arrays.equals(actualDigest, expectedDigest)) + functionData.returnValue = 1L; // true + else + functionData.returnValue = 0L; // false + } catch (NoSuchAlgorithmException e) { + throw new ExecutionException("No RIPEMD160 message digest service available", e); + } + } + }, + /** + * SHA256 A into B
+ * 0x0204 + */ + SHA256_A_TO_B(0x0204, 0, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + ByteBuffer messageByteBuffer = ByteBuffer.allocate(4 * MachineState.VALUE_SIZE); + messageByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + + messageByteBuffer.putLong(state.a1); + messageByteBuffer.putLong(state.a2); + messageByteBuffer.putLong(state.a3); + messageByteBuffer.putLong(state.a4); + + byte[] message = messageByteBuffer.array(); + + try { + MessageDigest digester = MessageDigest.getInstance("SHA-256"); + byte[] digest = digester.digest(message); + + ByteBuffer digestByteBuffer = ByteBuffer.wrap(digest); + digestByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + + state.b1 = digestByteBuffer.getLong(); + state.b2 = digestByteBuffer.getLong(); + state.b3 = digestByteBuffer.getLong(); + state.b4 = digestByteBuffer.getLong(); + } catch (NoSuchAlgorithmException e) { + throw new ExecutionException("No SHA-256 message digest service available", e); + } + } + }, + /** + * Check SHA256 of A matches B
+ * 0x0205
+ * Returns 1 if true, 0 if false + */ + CHECK_SHA256_A_WITH_B(0x0205, 0, true) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + ByteBuffer messageByteBuffer = ByteBuffer.allocate(4 * MachineState.VALUE_SIZE); + messageByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + + messageByteBuffer.putLong(state.a1); + messageByteBuffer.putLong(state.a2); + messageByteBuffer.putLong(state.a3); + messageByteBuffer.putLong(state.a4); + + byte[] message = messageByteBuffer.array(); + + try { + MessageDigest digester = MessageDigest.getInstance("SHA-256"); + byte[] actualDigest = digester.digest(message); + + ByteBuffer digestByteBuffer = ByteBuffer.allocate(4 * MachineState.VALUE_SIZE); + digestByteBuffer.order(ByteOrder.LITTLE_ENDIAN); + + digestByteBuffer.putLong(state.b1); + digestByteBuffer.putLong(state.b2); + digestByteBuffer.putLong(state.b3); + digestByteBuffer.putLong(state.b4); + + byte[] expectedDigest = digestByteBuffer.array(); + + if (Arrays.equals(actualDigest, expectedDigest)) + functionData.returnValue = 1L; // true + else + functionData.returnValue = 0L; // false + } catch (NoSuchAlgorithmException e) { + throw new ExecutionException("No SHA256 message digest service available", e); + } + } + }, + /** + * 0x0300
+ * Returns current block's "timestamp" + */ + GET_BLOCK_TIMESTAMP(0x0300, 0, true) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + functionData.returnValue = Timestamp.toLong(state.api.getCurrentBlockHeight(), 0); + } + }, + /** + * 0x0301
+ * Returns AT's creation block's "timestamp" + */ + GET_CREATION_TIMESTAMP(0x0301, 0, true) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + functionData.returnValue = Timestamp.toLong(state.api.getATCreationBlockHeight(state), 0); + } + }, + /** + * 0x0302
+ * Returns previous block's "timestamp" + */ + GET_PREVIOUS_BLOCK_TIMESTAMP(0x0302, 0, true) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + functionData.returnValue = Timestamp.toLong(state.api.getPreviousBlockHeight(), 0); + } + }, + /** + * 0x0303
+ * Put previous block's hash in A + */ + PUT_PREVIOUS_BLOCK_HASH_IN_A(0x0303, 0, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + state.api.putPreviousBlockHashInA(state); + } + }, + /** + * 0x0304
+ * Put transaction after timestamp in A, or zero if none
+ * a-k-a "A_To_Tx_After_Timestamp" + */ + PUT_TX_AFTER_TIMESTAMP_IN_A(0x0304, 1, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + state.api.putTransactionAfterTimestampInA(new Timestamp(functionData.value1), state); + } + }, + /** + * 0x0305
+ * Return transaction type from transaction in A
+ * Returns 0xffffffffffffffff in A not valid transaction + */ + GET_TYPE_FROM_TX_IN_A(0x0305, 0, true) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + functionData.returnValue = state.api.getTypeFromTransactionInA(state); + } + }, + /** + * 0x0306
+ * Return transaction amount from transaction in A
+ * Returns 0xffffffffffffffff in A not valid transaction + */ + GET_AMOUNT_FROM_TX_IN_A(0x0306, 0, true) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + functionData.returnValue = state.api.getAmountFromTransactionInA(state); + } + }, + /** + * 0x0307
+ * Return transaction timestamp from transaction in A
+ * Returns 0xffffffffffffffff in A not valid transaction + */ + GET_TIMESTAMP_FROM_TX_IN_A(0x0307, 0, true) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + functionData.returnValue = state.api.getTimestampFromTransactionInA(state); + } + }, + /** + * 0x0308
+ * Generate random number using transaction in A
+ * Returns 0xffffffffffffffff in A not valid transaction
+ * Can sleep to use next block as source of entropy + */ + GENERATE_RANDOM_USING_TX_IN_A(0x0308, 0, true) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + functionData.returnValue = state.api.generateRandomUsingTransactionInA(state); + + // If API set isSleeping then rewind program counter ready for being awoken + if (state.isSleeping) { + state.programCounter -= 1 + 2 + 4; // EXT_FUN_RET(1) + our function code(2) + address(4) + + // If specific sleep height not set, default to next block + if (state.sleepUntilHeight == null) + state.sleepUntilHeight = state.currentBlockHeight + 1; + } + } + }, + /** + * 0x0309
+ * Put 'message' from transaction in A into B
+ * If transaction has no 'message' then zero B
+ * Example 'message' could be 256-bit shared secret + */ + PUT_MESSAGE_FROM_TX_IN_A_INTO_B(0x0309, 0, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + state.api.putMessageFromTransactionInAIntoB(state); + } + }, + /** + * 0x030a
+ * Put sender/creator address from transaction in A into B + */ + PUT_ADDRESS_FROM_TX_IN_A_INTO_B(0x030a, 0, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + state.api.putAddressFromTransactionInAIntoB(state); + } + }, + /** + * 0x030b
+ * Put AT's creator's address into B + */ + PUT_CREATOR_INTO_B(0x030b, 0, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + state.api.putCreatorAddressIntoB(state); + } + }, + /** + * 0x0400
+ * Returns AT's current balance + */ + GET_CURRENT_BALANCE(0x0400, 0, true) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + functionData.returnValue = state.api.getCurrentBalance(state); + } + }, + /** + * 0x0401
+ * Returns AT's previous balance at end of last execution round
+ * Does not include any amounts sent to AT since + */ + GET_PREVIOUS_BALANCE(0x0401, 0, true) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + functionData.returnValue = state.api.getPreviousBalance(state); + } + }, + /** + * 0x0402
+ * Pay fee-inclusive amount to account address in B
+ * Reduces amount to current balance rather than failing due to insufficient funds + */ + PAY_TO_ADDRESS_IN_B(0x0402, 1, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + state.api.payAmountToB(functionData.value1, state); + } + }, + /** + * 0x0403
+ * Pay all remaining funds to account address in B + */ + PAY_ALL_TO_ADDRESS_IN_B(0x0403, 0, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + state.api.payCurrentBalanceToB(state); + } + }, + /** + * 0x0404
+ * Pay previous balance to account address in B
+ * Reduces amount to current balance rather than failing due to insufficient funds + */ + PAY_PREVIOUS_TO_ADDRESS_IN_B(0x0404, 0, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + state.api.payPreviousBalanceToB(state); + } + }, + /** + * 0x0405
+ * Send A as a message to address in B + */ + MESSAGE_A_TO_ADDRESS_IN_B(0x0405, 0, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + state.api.messageAToB(state); + } + }, + /** + * 0x0406
+ * Return 'timestamp' based on passed 'timestamp' plus minutes + */ + ADD_MINUTES_TO_TIMESTAMP(0x0406, 2, true) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + functionData.returnValue = state.api.addMinutesToTimestamp(new Timestamp(functionData.value1), functionData.value2, state); + } + }, + /** + * 0x0500 - 0x06ff
+ * Platform-specific functions.
+ * These are passed through to the API + */ + API_PASSTHROUGH(0x0500, 0, false) { + @Override + public void preExecuteCheck(int paramCount, boolean returnValueExpected, MachineState state, short rawFunctionCode) throws ExecutionException { + state.api.platformSpecificPreExecuteCheck(rawFunctionCode, paramCount, returnValueExpected); + } + + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + state.api.platformSpecificPostCheckExecute(rawFunctionCode, functionData, state); + } + }; + + public final short value; + public final int paramCount; + public final boolean returnsValue; + + private final static Map map = Arrays.stream(FunctionCode.values()) + .collect(Collectors.toMap(functionCode -> functionCode.value, functionCode -> functionCode)); + + private FunctionCode(int value, int paramCount, boolean returnsValue) { + this.value = (short) value; + this.paramCount = paramCount; + this.returnsValue = returnsValue; + } + + public static FunctionCode valueOf(int value) { + // Platform-specific? + if (value >= 0x0500 && value <= 0x06ff) + return API_PASSTHROUGH; + + return map.get((short) value); + } + + public void preExecuteCheck(int paramCount, boolean returnValueExpected, MachineState state, short rawFunctionCode) throws ExecutionException { + if (paramCount != this.paramCount) + throw new IllegalFunctionCodeException( + "Passed paramCount (" + paramCount + ") does not match function's required paramCount (" + this.paramCount + ")"); + + if (returnValueExpected != this.returnsValue) + throw new IllegalFunctionCodeException( + "Passed returnValueExpected (" + returnValueExpected + ") does not match function's return signature (" + this.returnsValue + ")"); + } + + /** + * Execute Function + *

+ * Can modify various fields of state, including programCounter. + *

+ * Throws a subclass of ExecutionException on error, e.g. InvalidAddressException. + * + * @param functionData + * @param state + * @throws ExecutionException + */ + public void execute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + // Check passed functionData against requirements of this function + preExecuteCheck(functionData.paramCount, functionData.returnValueExpected, state, rawFunctionCode); + + if (functionData.paramCount >= 1 && functionData.value1 == null) + throw new IllegalFunctionCodeException("Passed value1 is null but function has paramCount of (" + this.paramCount + ")"); + + if (functionData.paramCount == 2 && functionData.value2 == null) + throw new IllegalFunctionCodeException("Passed value2 is null but function has paramCount of (" + this.paramCount + ")"); + + state.logger.debug("Function \"" + this.name() + "\""); + + postCheckExecute(functionData, state, rawFunctionCode); + } + + /** Actually execute function */ + abstract protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException; + + // TODO: public abstract String disassemble(); + +} diff --git a/Java/src/org/ciyam/at/FunctionData.java b/Java/src/org/ciyam/at/FunctionData.java new file mode 100644 index 0000000..baf47df --- /dev/null +++ b/Java/src/org/ciyam/at/FunctionData.java @@ -0,0 +1,29 @@ +package org.ciyam.at; + +public class FunctionData { + public final int paramCount; + public final Long value1; + public final Long value2; + public final boolean returnValueExpected; + public Long returnValue; + + private FunctionData(int paramCount, Long value1, Long value2, boolean returnValueExpected) { + this.paramCount = paramCount; + this.value1 = value1; + this.value2 = value2; + this.returnValueExpected = returnValueExpected; + this.returnValue = null; + } + + public FunctionData(boolean returnValueExpected) { + this(0, null, null, returnValueExpected); + } + + public FunctionData(Long value, boolean returnValueExpected) { + this(1, value, null, returnValueExpected); + } + + public FunctionData(Long value1, Long value2, boolean returnValueExpected) { + this(2, value1, value2, returnValueExpected); + } +} diff --git a/Java/src/org/ciyam/at/IllegalFunctionCodeException.java b/Java/src/org/ciyam/at/IllegalFunctionCodeException.java new file mode 100644 index 0000000..c7956c3 --- /dev/null +++ b/Java/src/org/ciyam/at/IllegalFunctionCodeException.java @@ -0,0 +1,21 @@ +package org.ciyam.at; + +@SuppressWarnings("serial") +public class IllegalFunctionCodeException extends ExecutionException { + + public IllegalFunctionCodeException() { + } + + public IllegalFunctionCodeException(String message) { + super(message); + } + + public IllegalFunctionCodeException(Throwable cause) { + super(cause); + } + + public IllegalFunctionCodeException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/Java/src/org/ciyam/at/IllegalOperationException.java b/Java/src/org/ciyam/at/IllegalOperationException.java new file mode 100644 index 0000000..ae43602 --- /dev/null +++ b/Java/src/org/ciyam/at/IllegalOperationException.java @@ -0,0 +1,21 @@ +package org.ciyam.at; + +@SuppressWarnings("serial") +public class IllegalOperationException extends ExecutionException { + + public IllegalOperationException() { + } + + public IllegalOperationException(String message) { + super(message); + } + + public IllegalOperationException(Throwable cause) { + super(cause); + } + + public IllegalOperationException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/Java/src/org/ciyam/at/InvalidAddressException.java b/Java/src/org/ciyam/at/InvalidAddressException.java new file mode 100644 index 0000000..672dbfa --- /dev/null +++ b/Java/src/org/ciyam/at/InvalidAddressException.java @@ -0,0 +1,21 @@ +package org.ciyam.at; + +@SuppressWarnings("serial") +public class InvalidAddressException extends ExecutionException { + + public InvalidAddressException() { + } + + public InvalidAddressException(String message) { + super(message); + } + + public InvalidAddressException(Throwable cause) { + super(cause); + } + + public InvalidAddressException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/Java/src/org/ciyam/at/LoggerInterface.java b/Java/src/org/ciyam/at/LoggerInterface.java new file mode 100644 index 0000000..501493f --- /dev/null +++ b/Java/src/org/ciyam/at/LoggerInterface.java @@ -0,0 +1,11 @@ +package org.ciyam.at; + +public interface LoggerInterface { + + public void error(String message); + + public void debug(String message); + + public void echo(String message); + +} diff --git a/Java/src/org/ciyam/at/MachineState.java b/Java/src/org/ciyam/at/MachineState.java new file mode 100644 index 0000000..1175d42 --- /dev/null +++ b/Java/src/org/ciyam/at/MachineState.java @@ -0,0 +1,445 @@ +package org.ciyam.at; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +public class MachineState { + + /** Header bytes length */ + public static final int HEADER_LENGTH = 2 + 2 + 2 + 2 + 2 + 2; // version reserved code data call-stack user-stack + + /** Size of value stored in data segment - typically 8 bytes (long) */ + public static final int VALUE_SIZE = 8; + + /** Size of code-address - typically 4 bytes (int) */ + public static final int ADDRESS_SIZE = 4; + + /** Maximum value for an address in the code segment */ + public static final int MAX_CODE_ADDRESS = 0x1fffffff; + + /** Bytes per code page */ + public static final int CODE_PAGE_SIZE = 1; + + /** Bytes per data page */ + public static final int DATA_PAGE_SIZE = VALUE_SIZE; + + /** Bytes per call stack page */ + public static final int CALL_STACK_PAGE_SIZE = ADDRESS_SIZE; + + /** Bytes per user stack page */ + public static final int USER_STACK_PAGE_SIZE = VALUE_SIZE; + + /** Program Counter: offset into code to point of current execution */ + public int programCounter; + + /** Initial program counter value to use on next block after current block's execution has stopped. 0 by default */ + public int onStopAddress; + + /** Program counter value to use if an error occurs during execution. If null upon error, refund all funds to creator and finish */ + public Integer onErrorAddress; + + /** Execution for current block has stopped. Continue at current program counter on next/specific block */ + public boolean isSleeping; + + /** Block height required to wake from sleeping, or null if not in use */ + public Integer sleepUntilHeight; + + /** Execution for current block has stopped. Restart at onStopAddress on next block */ + public boolean isStopped; + + /** Execution stopped due to lack of funds for processing. Restart at onStopAddress if frozenBalance increases */ + public boolean isFrozen; + + /** Balance at which there were not enough funds, or null if not in use */ + public Long frozenBalance; + + /** Execution permanently stopped */ + public boolean isFinished; + + /** Execution permanently stopped due to fatal error */ + public boolean hadFatalError; + + // 256-bit pseudo-registers + public long a1; + public long a2; + public long a3; + public long a4; + + public long b1; + public long b2; + public long b3; + public long b4; + + public int currentBlockHeight; + + /** Number of opcodes processed this execution */ + public int steps; + + public API api; + LoggerInterface logger; + + public short version; + public short reserved; + public short numCodePages; + public short numDataPages; + public short numCallStackPages; + public short numUserStackPages; + + public byte[] headerBytes; + + public ByteBuffer codeByteBuffer; + public ByteBuffer dataByteBuffer; + public ByteBuffer callStackByteBuffer; + public ByteBuffer userStackByteBuffer; + + private class Flags { + private int flags; + + public Flags() { + flags = 0; + } + + public Flags(int value) { + this.flags = value; + } + + public void push(boolean flag) { + flags <<= 1; + flags |= flag ? 1 : 0; + } + + public boolean pop() { + boolean result = (flags & 1) != 0; + flags >>>= 1; + return result; + } + + public int intValue() { + return flags; + } + } + + /** For internal use when recreating a machine state */ + private MachineState(API api, LoggerInterface logger, byte[] headerBytes) { + if (headerBytes.length != HEADER_LENGTH) + throw new IllegalArgumentException("headerBytes length " + headerBytes.length + " incorrect, expected " + HEADER_LENGTH); + + this.headerBytes = headerBytes; + parseHeader(); + + this.codeByteBuffer = ByteBuffer.allocate(this.numCodePages * CODE_PAGE_SIZE).order(ByteOrder.LITTLE_ENDIAN); + + this.dataByteBuffer = ByteBuffer.allocate(this.numDataPages * DATA_PAGE_SIZE).order(ByteOrder.LITTLE_ENDIAN); + + this.callStackByteBuffer = ByteBuffer.allocate(this.numCallStackPages * CALL_STACK_PAGE_SIZE).order(ByteOrder.LITTLE_ENDIAN); + this.callStackByteBuffer.position(this.callStackByteBuffer.limit()); // Downward-growing stack, so start at the end + + this.userStackByteBuffer = ByteBuffer.allocate(this.numUserStackPages * USER_STACK_PAGE_SIZE).order(ByteOrder.LITTLE_ENDIAN); + this.userStackByteBuffer.position(this.userStackByteBuffer.limit()); // Downward-growing stack, so start at the end + + this.api = api; + this.currentBlockHeight = api.getCurrentBlockHeight(); + this.steps = 0; + this.logger = logger; + } + + /** For creating a new machine state */ + public MachineState(API api, LoggerInterface logger, byte[] headerBytes, byte[] codeBytes, byte[] dataBytes) { + this(api, logger, headerBytes); + + // XXX: Why don't we simply ByteBuffer.wrap(codeBytes) as they're read-only? + // This would do away with the need to specify numCodePages, save space and provide automatic end-of-code detection during execution thanks to + // ByteBuffer's BufferUnderflowException + + if (codeBytes.length > this.numCodePages * CODE_PAGE_SIZE) + throw new IllegalArgumentException("Number of code pages too small to hold code bytes"); + + if (dataBytes.length > this.numDataPages * DATA_PAGE_SIZE) + throw new IllegalArgumentException("Number of data pages too small to hold data bytes"); + + System.arraycopy(codeBytes, 0, this.codeByteBuffer.array(), 0, codeBytes.length); + + System.arraycopy(dataBytes, 0, this.dataByteBuffer.array(), 0, dataBytes.length); + + this.programCounter = 0; + this.onStopAddress = 0; + this.onErrorAddress = null; + this.isSleeping = false; + this.sleepUntilHeight = null; + this.isStopped = false; + this.isFinished = false; + this.hadFatalError = false; + this.isFrozen = false; + this.frozenBalance = null; + } + + /** For serializing a machine state */ + public byte[] toBytes() { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + try { + // Header first + bytes.write(this.headerBytes); + + // Code + bytes.write(this.codeByteBuffer.array()); + + // Data + bytes.write(this.dataByteBuffer.array()); + + // Call stack length (32bit unsigned int) + int callStackLength = this.callStackByteBuffer.limit() - this.callStackByteBuffer.position(); + bytes.write(toByteArray(callStackLength)); + // Call stack + bytes.write(this.callStackByteBuffer.array(), this.callStackByteBuffer.position(), callStackLength); + + // User stack length (32bit unsigned int) + int userStackLength = this.userStackByteBuffer.limit() - this.userStackByteBuffer.position(); + bytes.write(toByteArray(userStackLength)); + // User stack + bytes.write(this.userStackByteBuffer.array(), this.userStackByteBuffer.position(), userStackLength); + + // Actual state + bytes.write(toByteArray(this.programCounter)); + bytes.write(toByteArray(this.onStopAddress)); + + // Various flags + Flags flags = new Flags(); + flags.push(this.isSleeping); + flags.push(this.isStopped); + flags.push(this.isFinished); + flags.push(this.hadFatalError); + flags.push(this.isFrozen); + + flags.push(this.onErrorAddress != null); // has onErrorAddress? + flags.push(this.sleepUntilHeight != null); // has sleepUntilHeight? + flags.push(this.frozenBalance != null); // has frozenBalance? + + boolean hasNonZeroA = this.a1 != 0 || this.a2 != 0 || this.a3 != 0 || this.a4 != 0; + flags.push(hasNonZeroA); + + boolean hasNonZeroB = this.b1 != 0 || this.b2 != 0 || this.b3 != 0 || this.b4 != 0; + flags.push(hasNonZeroB); + + bytes.write(toByteArray(flags.intValue())); + + // Optional flag-indicated extra info in same order as above + if (this.onErrorAddress != null) + bytes.write(toByteArray(this.onErrorAddress)); + + if (this.sleepUntilHeight != null) + bytes.write(toByteArray(this.sleepUntilHeight)); + + if (this.frozenBalance != null) + bytes.write(toByteArray(this.frozenBalance)); + + if (hasNonZeroA) { + bytes.write(toByteArray(this.a1)); + bytes.write(toByteArray(this.a2)); + bytes.write(toByteArray(this.a3)); + bytes.write(toByteArray(this.a4)); + } + + if (hasNonZeroB) { + bytes.write(toByteArray(this.b1)); + bytes.write(toByteArray(this.b2)); + bytes.write(toByteArray(this.b3)); + bytes.write(toByteArray(this.b4)); + } + } catch (IOException e) { + return null; + } + + return bytes.toByteArray(); + } + + /** For restoring a previously serialized machine state */ + public static MachineState fromBytes(API api, LoggerInterface logger, byte[] bytes) { + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); + + byte[] headerBytes = new byte[HEADER_LENGTH]; + byteBuffer.get(headerBytes); + + MachineState state = new MachineState(api, logger, headerBytes); + + byte[] codeBytes = new byte[state.codeByteBuffer.capacity()]; + byteBuffer.get(codeBytes); + System.arraycopy(codeBytes, 0, state.codeByteBuffer.array(), 0, codeBytes.length); + + byte[] dataBytes = new byte[state.dataByteBuffer.capacity()]; + byteBuffer.get(dataBytes); + System.arraycopy(dataBytes, 0, state.dataByteBuffer.array(), 0, dataBytes.length); + + int callStackLength = byteBuffer.getInt(); + byte[] callStackBytes = new byte[callStackLength]; + byteBuffer.get(callStackBytes); + // Restore call stack pointer, and useful for copy below + state.callStackByteBuffer.position(state.callStackByteBuffer.limit() - callStackLength); + // Call stack grows downwards so copy to end + System.arraycopy(callStackBytes, 0, state.callStackByteBuffer.array(), state.callStackByteBuffer.position(), callStackLength); + + int userStackLength = byteBuffer.getInt(); + byte[] userStackBytes = new byte[userStackLength]; + byteBuffer.get(userStackBytes); + // Restore user stack pointer, and useful for copy below + state.userStackByteBuffer.position(state.userStackByteBuffer.limit() - userStackLength); + // User stack grows downwards so copy to end + System.arraycopy(userStackBytes, 0, state.userStackByteBuffer.array(), state.userStackByteBuffer.position(), userStackLength); + + // Actual state + state.programCounter = byteBuffer.getInt(); + state.onStopAddress = byteBuffer.getInt(); + + // Various flags (reverse order to toBytes) + Flags flags = state.new Flags(byteBuffer.getInt()); + boolean hasNonZeroB = flags.pop(); + boolean hasNonZeroA = flags.pop(); + boolean hasFrozenBalance = flags.pop(); + boolean hasSleepUntilHeight = flags.pop(); + boolean hasOnErrorAddress = flags.pop(); + + state.isFrozen = flags.pop(); + state.hadFatalError = flags.pop(); + state.isFinished = flags.pop(); + state.isStopped = flags.pop(); + state.isSleeping = flags.pop(); + + // Optional extras (same order as toBytes) + if (hasOnErrorAddress) + state.onErrorAddress = byteBuffer.getInt(); + + if (hasSleepUntilHeight) + state.sleepUntilHeight = byteBuffer.getInt(); + + if (hasFrozenBalance) + state.frozenBalance = byteBuffer.getLong(); + + if (hasNonZeroA) { + state.a1 = byteBuffer.getLong(); + state.a2 = byteBuffer.getLong(); + state.a3 = byteBuffer.getLong(); + state.a4 = byteBuffer.getLong(); + } + + if (hasNonZeroB) { + state.b1 = byteBuffer.getLong(); + state.b2 = byteBuffer.getLong(); + state.b3 = byteBuffer.getLong(); + state.b4 = byteBuffer.getLong(); + } + + return state; + } + + /** Convert int to little-endian byte array */ + private byte[] toByteArray(int value) { + return new byte[] { (byte) (value), (byte) (value >> 8), (byte) (value >> 16), (byte) (value >> 24) }; + } + + /** Convert long to little-endian byte array */ + private byte[] toByteArray(long value) { + return new byte[] { (byte) (value), (byte) (value >> 8), (byte) (value >> 16), (byte) (value >> 24), (byte) (value >> 32), (byte) (value >> 40), + (byte) (value >> 48), (byte) (value >> 56) }; + } + + private void parseHeader() { + ByteBuffer byteBuffer = ByteBuffer.wrap(this.headerBytes); + byteBuffer.order(ByteOrder.LITTLE_ENDIAN); + + this.version = byteBuffer.getShort(); + if (this.version < 1) + throw new IllegalArgumentException("Version must be >= 0"); + + this.reserved = byteBuffer.getShort(); + + this.numCodePages = byteBuffer.getShort(); + if (this.numCodePages < 1) + throw new IllegalArgumentException("Number of code pages must be > 0"); + + this.numDataPages = byteBuffer.getShort(); + if (this.numDataPages < 1) + throw new IllegalArgumentException("Number of data pages must be > 0"); + + this.numCallStackPages = byteBuffer.getShort(); + if (this.numCallStackPages < 0) + throw new IllegalArgumentException("Number of call stack pages must be >= 0"); + + this.numUserStackPages = byteBuffer.getShort(); + if (this.numUserStackPages < 0) + throw new IllegalArgumentException("Number of user stack pages must be >= 0"); + } + + public void execute() { + // Set byte buffer position using program counter + codeByteBuffer.position(this.programCounter); + + // Reset for this round of execution + this.isSleeping = false; + this.sleepUntilHeight = null; + this.isStopped = false; + this.isFrozen = false; + this.frozenBalance = null; + this.steps = 0; + + while (!this.isSleeping && !this.isStopped && !this.isFinished && !this.isFrozen) { + byte rawOpCode = codeByteBuffer.get(); + OpCode nextOpCode = OpCode.valueOf(rawOpCode); + + try { + if (nextOpCode == null) + throw new IllegalOperationException("OpCode 0x" + String.format("%02x", rawOpCode) + " not recognised"); + + this.logger.debug("[PC: " + String.format("%04x", this.programCounter) + "] " + nextOpCode.name()); + + nextOpCode.execute(codeByteBuffer, dataByteBuffer, userStackByteBuffer, callStackByteBuffer, this); + this.programCounter = codeByteBuffer.position(); + } catch (ExecutionException e) { + this.logger.debug("Error at PC " + String.format("%04x", this.programCounter) + ": " + e.getMessage()); + + if (this.onErrorAddress == null) { + this.isFinished = true; + this.hadFatalError = true; + this.api.onFatalError(this, e); + break; + } + + this.programCounter = this.onErrorAddress; + codeByteBuffer.position(this.programCounter); + } + + ++this.steps; + } + + if (this.isStopped) { + this.logger.debug("Setting program counter to stop address: " + String.format("%04x", this.onStopAddress)); + this.programCounter = this.onStopAddress; + } + } + + // public String disassemble(List dataLabels, Map codeLabels) { + public String disassemble() throws ExecutionException { + String output = ""; + + codeByteBuffer.position(0); + + while (codeByteBuffer.hasRemaining()) { + byte rawOpCode = codeByteBuffer.get(); + if (rawOpCode == 0) + continue; + + OpCode nextOpCode = OpCode.valueOf(rawOpCode); + if (nextOpCode == null) + throw new IllegalOperationException("OpCode 0x" + String.format("%02x", rawOpCode) + " not recognised"); + + if (!output.isEmpty()) + output += "\n"; + + output += "[PC: " + String.format("%04x", codeByteBuffer.position() - 1) + "] " + nextOpCode.disassemble(codeByteBuffer, dataByteBuffer); + } + + return output; + } + +} diff --git a/Java/src/org/ciyam/at/OpCode.java b/Java/src/org/ciyam/at/OpCode.java new file mode 100644 index 0000000..8e69040 --- /dev/null +++ b/Java/src/org/ciyam/at/OpCode.java @@ -0,0 +1,1014 @@ +package org.ciyam.at; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * This enum contains op codes for the CIYAM AT machine. + *

+ * Op codes are represented by a single byte and maybe be followed by additional arguments like data addresses, offset, immediate values, etc. + *

+ * OpCode instances can be obtained via the default OpCode.valueOf(String) or the additional OpCode.valueOf(int). + *

+ * Use the OpCode.execute method to perform the operation. + *

+ * In the documentation for each OpCode: + *

+ * @addr means "store at addr" + *

+ * $addr means "fetch from addr" + *

+ * @($addr) means "store at address fetched from addr", i.e. indirect + *

+ * $($addr1 + $addr2) means "fetch from address fetched from addr1 plus offset fetched from addr2", i.e. indirect indexed + * + * @see OpCode#valueOf(int) + * @see OpCode#execute(ByteBuffer, ByteBuffer, ByteBuffer, ByteBuffer, MachineState) + */ +public enum OpCode { + + /** + * No OPeration
+ * 0x7f
+ * (Does nothing) + */ + NOP(0x7f) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) { + // Do nothing + } + }, + /** + * SET VALue
+ * 0x01 addr value
+ * @addr = value + */ + SET_VAL(0x01, OpCodeParam.DEST_ADDR, OpCodeParam.VALUE) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + int address = Utils.getDataAddress(codeByteBuffer, dataByteBuffer); + long value = Utils.getCodeValue(codeByteBuffer); + dataByteBuffer.putLong(address, value); + } + }, + /** + * SET DATa
+ * 0x02 addr1 addr2
+ * @addr1 = $addr2 + */ + SET_DAT(0x02, OpCodeParam.DEST_ADDR, OpCodeParam.SRC_ADDR) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + int address1 = Utils.getDataAddress(codeByteBuffer, dataByteBuffer); + int address2 = Utils.getDataAddress(codeByteBuffer, dataByteBuffer); + long value = dataByteBuffer.getLong(address2); + dataByteBuffer.putLong(address1, value); + } + }, + /** + * CLeaR DATa
+ * 0x03 addr
+ * @addr = 0 + */ + CLR_DAT(0x03, OpCodeParam.DEST_ADDR) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + int address = Utils.getDataAddress(codeByteBuffer, dataByteBuffer); + dataByteBuffer.putLong(address, 0L); + } + }, + /** + * INCrement DATa
+ * 0x04 addr
+ * @addr += 1 + */ + INC_DAT(0x04, OpCodeParam.DEST_ADDR) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + int address = Utils.getDataAddress(codeByteBuffer, dataByteBuffer); + long value = dataByteBuffer.getLong(address); + dataByteBuffer.putLong(address, value + 1); + } + }, + /** + * DECrement DATa
+ * 0x05 addr
+ * @addr -= 1 + */ + DEC_DAT(0x05, OpCodeParam.DEST_ADDR) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + int address = Utils.getDataAddress(codeByteBuffer, dataByteBuffer); + long value = dataByteBuffer.getLong(address); + dataByteBuffer.putLong(address, value - 1); + } + }, + /** + * ADD DATa
+ * 0x06 addr1 addr2
+ * @addr1 += $addr2 + */ + ADD_DAT(0x06, OpCodeParam.DEST_ADDR, OpCodeParam.SRC_ADDR) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + executeDataOperation(codeByteBuffer, dataByteBuffer, (a, b) -> a + b); + } + }, + /** + * SUBtract DATa
+ * 0x07 addr1 addr2
+ * @addr1 -= $addr2 + */ + SUB_DAT(0x07, OpCodeParam.DEST_ADDR, OpCodeParam.SRC_ADDR) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + executeDataOperation(codeByteBuffer, dataByteBuffer, (a, b) -> a - b); + } + }, + /** + * MULtiply DATa
+ * 0x08 addr1 addr2
+ * @addr1 *= $addr2 + */ + MUL_DAT(0x08, OpCodeParam.DEST_ADDR, OpCodeParam.SRC_ADDR) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + executeDataOperation(codeByteBuffer, dataByteBuffer, (a, b) -> a * b); + } + }, + /** + * DIVide DATa
+ * 0x09 addr1 addr2
+ * @addr1 /= $addr2
+ * Can also throw IllegealOperationException if divide-by-zero attempted. + */ + DIV_DAT(0x09, OpCodeParam.DEST_ADDR, OpCodeParam.SRC_ADDR) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + try { + executeDataOperation(codeByteBuffer, dataByteBuffer, (a, b) -> a / b); + } catch (ArithmeticException e) { + throw new IllegalOperationException("Divide by zero", e); + } + } + }, + /** + * Binary-OR DATa
+ * 0x0a addr1 addr2
+ * @addr1 |= $addr2 + */ + BOR_DAT(0x0a, OpCodeParam.DEST_ADDR, OpCodeParam.SRC_ADDR) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + executeDataOperation(codeByteBuffer, dataByteBuffer, (a, b) -> a | b); + } + }, + /** + * Binary-AND DATa
+ * 0x0b addr1 addr2
+ * @addr1 &= $addr2 + */ + AND_DAT(0x0b, OpCodeParam.DEST_ADDR, OpCodeParam.SRC_ADDR) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + executeDataOperation(codeByteBuffer, dataByteBuffer, (a, b) -> a & b); + } + }, + /** + * EXclusive OR DATa
+ * 0x0c addr1 addr2
+ * @addr1 ^= $addr2 + */ + XOR_DAT(0x0c, OpCodeParam.DEST_ADDR, OpCodeParam.SRC_ADDR) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + executeDataOperation(codeByteBuffer, dataByteBuffer, (a, b) -> a ^ b); + } + }, + /** + * Bitwise-NOT DATa
+ * 0x0d addr
+ * @addr = ~$addr + */ + NOT_DAT(0x0d, OpCodeParam.DEST_ADDR) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + int address = Utils.getDataAddress(codeByteBuffer, dataByteBuffer); + long value = dataByteBuffer.getLong(address); + dataByteBuffer.putLong(address, ~value); + } + }, + /** + * SET using INDirect data
+ * 0x0e addr1 addr2
+ * @addr1 = $($addr2) + */ + SET_IND(0x0e, OpCodeParam.DEST_ADDR, OpCodeParam.INDIRECT_SRC_ADDR) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + int address1 = Utils.getDataAddress(codeByteBuffer, dataByteBuffer); + int address2 = Utils.getDataAddress(codeByteBuffer, dataByteBuffer); + + long address3 = dataByteBuffer.getLong(address2) * MachineState.VALUE_SIZE; + + if (address3 < 0 || address3 + MachineState.VALUE_SIZE >= dataByteBuffer.limit()) + throw new InvalidAddressException("Data address out of bounds"); + + long value = dataByteBuffer.getLong((int) address3); + dataByteBuffer.putLong(address1, value); + } + }, + /** + * SET using indirect InDeXed data
+ * 0x0f addr1 addr2 addr3
+ * @addr1 = $($addr2 + $addr3) + */ + SET_IDX(0x0f, OpCodeParam.DEST_ADDR, OpCodeParam.INDIRECT_SRC_ADDR_WITH_INDEX, OpCodeParam.INDEX) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + int address1 = Utils.getDataAddress(codeByteBuffer, dataByteBuffer); + int address2 = Utils.getDataAddress(codeByteBuffer, dataByteBuffer); + int address3 = Utils.getDataAddress(codeByteBuffer, dataByteBuffer); + + long baseAddress = dataByteBuffer.getLong(address2) * MachineState.VALUE_SIZE; + long offset = dataByteBuffer.getLong(address3) * MachineState.VALUE_SIZE; + + long newAddress = baseAddress + offset; + + if (newAddress < 0 || newAddress + MachineState.VALUE_SIZE >= dataByteBuffer.limit()) + throw new InvalidAddressException("Data address out of bounds"); + + long value = dataByteBuffer.getLong((int) newAddress); + dataByteBuffer.putLong(address1, value); + } + }, + /** + * PuSH DATa onto user stack
+ * 0x10 addr
+ * @--user_stack = $addr
+ * Can also throw StackBoundsException if user stack exhausted. + */ + PSH_DAT(0x10, OpCodeParam.SRC_ADDR) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + int address = Utils.getDataAddress(codeByteBuffer, dataByteBuffer); + long value = dataByteBuffer.getLong(address); + + try { + // Simulate backwards-walking stack + int newPosition = userStackByteBuffer.position() - MachineState.VALUE_SIZE; + userStackByteBuffer.putLong(newPosition, value); + userStackByteBuffer.position(newPosition); + } catch (IndexOutOfBoundsException | IllegalArgumentException e) { + throw new StackBoundsException("No room on user stack to push data", e); + } + } + }, + /** + * POP DATa from user stack
+ * 0x11 addr
+ * @addr = $user_stack++
+ * Can also throw StackBoundsException if user stack empty. + */ + POP_DAT(0x11, OpCodeParam.DEST_ADDR) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + int address = Utils.getDataAddress(codeByteBuffer, dataByteBuffer); + + try { + long value = userStackByteBuffer.getLong(); + + // Clear old stack entry + userStackByteBuffer.putLong(userStackByteBuffer.position() - MachineState.VALUE_SIZE, 0L); + + // Put popped value into data address + dataByteBuffer.putLong(address, value); + } catch (BufferUnderflowException e) { + throw new StackBoundsException("Empty user stack from which to pop data", e); + } + } + }, + /** + * JuMP into SUBroutine
+ * 0x12 addr
+ * @--call_stack = PC after opcode & args, PC = addr
+ * Can also throw StackBoundsException if call stack exhausted. + */ + JMP_SUB(0x12, OpCodeParam.CODE_ADDR) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + int address = Utils.getCodeAddress(codeByteBuffer); + + try { + // Simulate backwards-walking stack + int newPosition = callStackByteBuffer.position() - MachineState.ADDRESS_SIZE; + callStackByteBuffer.putInt(newPosition, codeByteBuffer.position()); + callStackByteBuffer.position(newPosition); + } catch (IndexOutOfBoundsException | IllegalArgumentException e) { + throw new StackBoundsException("No room on call stack to call subroutine", e); + } + + codeByteBuffer.position(address); + } + }, + /** + * RETurn from SUBroutine
+ * 0x13
+ * PC = $call_stack++
+ * Can also throw StackBoundsException if call stack empty. + */ + RET_SUB(0x13) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + try { + int returnAddress = callStackByteBuffer.getInt(); + + // Clear old stack entry + callStackByteBuffer.putInt(callStackByteBuffer.position() - MachineState.ADDRESS_SIZE, 0); + + codeByteBuffer.position(returnAddress); + } catch (BufferUnderflowException e) { + throw new StackBoundsException("Empty call stack missing return address from subroutine", e); + } + } + }, + /** + * Store INDirect DATa
+ * 0x14 addr1 addr2
+ * @($addr1) = $addr2 + */ + IND_DAT(0x14, OpCodeParam.INDIRECT_DEST_ADDR, OpCodeParam.SRC_ADDR) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + int address1 = Utils.getDataAddress(codeByteBuffer, dataByteBuffer); + int address2 = Utils.getDataAddress(codeByteBuffer, dataByteBuffer); + + long address3 = dataByteBuffer.getLong(address1) * MachineState.VALUE_SIZE; + + if (address3 < 0 || address3 + MachineState.VALUE_SIZE >= dataByteBuffer.limit()) + throw new InvalidAddressException("Data address out of bounds"); + + long value = dataByteBuffer.getLong(address2); + dataByteBuffer.putLong((int) address3, value); + } + }, + /** + * Store indirect InDeXed DATa
+ * 0x15 addr1 addr2
+ * @($addr1 + $addr2) = $addr3 + */ + IDX_DAT(0x15, OpCodeParam.INDIRECT_DEST_ADDR_WITH_INDEX, OpCodeParam.INDEX, OpCodeParam.SRC_ADDR) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + int address1 = Utils.getDataAddress(codeByteBuffer, dataByteBuffer); + int address2 = Utils.getDataAddress(codeByteBuffer, dataByteBuffer); + int address3 = Utils.getDataAddress(codeByteBuffer, dataByteBuffer); + + long baseAddress = dataByteBuffer.getLong(address1) * MachineState.VALUE_SIZE; + long offset = dataByteBuffer.getLong(address2) * MachineState.VALUE_SIZE; + + long newAddress = baseAddress + offset; + + if (newAddress < 0 || newAddress + MachineState.VALUE_SIZE >= dataByteBuffer.limit()) + throw new InvalidAddressException("Data address out of bounds"); + + long value = dataByteBuffer.getLong(address3); + dataByteBuffer.putLong((int) newAddress, value); + } + }, + /** + * MODulo DATa
+ * 0x16 addr1 addr2
+ * @addr1 %= $addr2 + */ + MOD_DAT(0x16, OpCodeParam.DEST_ADDR, OpCodeParam.SRC_ADDR) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + try { + executeDataOperation(codeByteBuffer, dataByteBuffer, (a, b) -> a % b); + } catch (ArithmeticException e) { + throw new IllegalOperationException("Divide by zero", e); + } + } + }, + /** + * SHift Left DATa
+ * 0x17 addr1 addr2
+ * @addr1 <<= $addr2 + */ + SHL_DAT(0x17, OpCodeParam.DEST_ADDR, OpCodeParam.SRC_ADDR) { + private static final long MAX_SHIFT = MachineState.VALUE_SIZE * 8; + + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + // If 2nd arg is more than value size (in bits) then return 0 to simulate all bits being shifted out of existence + executeDataOperation(codeByteBuffer, dataByteBuffer, (a, b) -> b >= MAX_SHIFT ? 0 : a << b); + } + }, + /** + * SHift Right DATa
+ * 0x18 addr1 addr2
+ * @addr1 >>= $addr2
+ * Note: new MSB bit will be zero + */ + SHR_DAT(0x18, OpCodeParam.DEST_ADDR, OpCodeParam.SRC_ADDR) { + private static final long MAX_SHIFT = MachineState.VALUE_SIZE * 8; + + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + // If 2nd arg is more than value size (in bits) then return 0 to simulate all bits being shifted out of existence + executeDataOperation(codeByteBuffer, dataByteBuffer, (a, b) -> b >= MAX_SHIFT ? 0 : a >>> b); + } + }, + /** + * JuMP to ADdRess
+ * 0x1a addr
+ * PC = addr + */ + JMP_ADR(0x1a, OpCodeParam.CODE_ADDR) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + int address = Utils.getCodeAddress(codeByteBuffer); + + codeByteBuffer.position(address); + } + }, + /** + * Branch if ZeRo
+ * 0x1b addr offset
+ * if ($addr == 0) PC += offset
+ * Note: PC is considered to be immediately before opcode byte. + */ + BZR_DAT(0x1b, OpCodeParam.SRC_ADDR, OpCodeParam.OFFSET) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + int opCodePosition = codeByteBuffer.position() - 1; // i.e. before this OpCode + + int address = Utils.getDataAddress(codeByteBuffer, dataByteBuffer); + byte offset = Utils.getCodeOffset(codeByteBuffer); + + int branchTarget = opCodePosition + offset; + + if (branchTarget < 0 || branchTarget >= codeByteBuffer.limit()) + throw new InvalidAddressException("branch target out of bounds"); + + long value = dataByteBuffer.getLong(address); + + if (value == 0) + codeByteBuffer.position(branchTarget); + } + }, + /** + * Branch if Not Zero
+ * 0x1e addr offset
+ * if ($addr != 0) PC += offset
+ * Note: PC is considered to be immediately before opcode byte. + */ + BNZ_DAT(0x1e, OpCodeParam.SRC_ADDR, OpCodeParam.OFFSET) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + int opCodePosition = codeByteBuffer.position() - 1; // i.e. before this OpCode + + int address = Utils.getDataAddress(codeByteBuffer, dataByteBuffer); + byte offset = Utils.getCodeOffset(codeByteBuffer); + + int branchTarget = opCodePosition + offset; + + if (branchTarget < 0 || branchTarget >= codeByteBuffer.limit()) + throw new InvalidAddressException("branch target out of bounds"); + + long value = dataByteBuffer.getLong(address); + + if (value != 0) + codeByteBuffer.position(branchTarget); + } + }, + /** + * Branch if Greater-Than DATa
+ * 0x1f addr1 addr2 offset
+ * if ($addr1 > $addr2) PC += offset
+ * Note: PC is considered to be immediately before opcode byte. + */ + BGT_DAT(0x1f, OpCodeParam.SRC_ADDR, OpCodeParam.SRC_ADDR, OpCodeParam.OFFSET) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + executeBranchConditional(codeByteBuffer, dataByteBuffer, state, (a, b) -> a > b); + } + }, + /** + * Branch if Less-Than DATa
+ * 0x20 addr1 addr2 offset
+ * if ($addr1 < $addr2) PC += offset
+ * Note: PC is considered to be immediately before opcode byte. + */ + BLT_DAT(0x20, OpCodeParam.SRC_ADDR, OpCodeParam.SRC_ADDR, OpCodeParam.OFFSET) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + executeBranchConditional(codeByteBuffer, dataByteBuffer, state, (a, b) -> a < b); + } + }, + /** + * Branch if Greater-or-Equal DATa
+ * 0x21 addr1 addr2 offset
+ * if ($addr1 >= $addr2) PC += offset
+ * Note: PC is considered to be immediately before opcode byte. + */ + BGE_DAT(0x21, OpCodeParam.SRC_ADDR, OpCodeParam.SRC_ADDR, OpCodeParam.OFFSET) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + executeBranchConditional(codeByteBuffer, dataByteBuffer, state, (a, b) -> a >= b); + } + }, + /** + * Branch if Less-or-Equal DATa
+ * 0x22 addr1 addr2 offset
+ * if ($addr1 <= $addr2) PC += offset
+ * Note: PC is considered to be immediately before opcode byte. + */ + BLE_DAT(0x22, OpCodeParam.SRC_ADDR, OpCodeParam.SRC_ADDR, OpCodeParam.OFFSET) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + executeBranchConditional(codeByteBuffer, dataByteBuffer, state, (a, b) -> a <= b); + } + }, + /** + * Branch if EQual DATa
+ * 0x23 addr1 addr2 offset
+ * if ($addr1 == $addr2) PC += offset
+ * Note: PC is considered to be immediately before opcode byte. + */ + BEQ_DAT(0x23, OpCodeParam.SRC_ADDR, OpCodeParam.SRC_ADDR, OpCodeParam.OFFSET) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + executeBranchConditional(codeByteBuffer, dataByteBuffer, state, (a, b) -> a == b); + } + }, + /** + * Branch if Not-Equal DATa
+ * 0x24 addr1 addr2 offset
+ * if ($addr1 != $addr2) PC += offset
+ * Note: PC is considered to be immediately before opcode byte. + */ + BNE_DAT(0x24, OpCodeParam.SRC_ADDR, OpCodeParam.SRC_ADDR, OpCodeParam.OFFSET) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + executeBranchConditional(codeByteBuffer, dataByteBuffer, state, (a, b) -> a != b); + } + }, + /** + * SLeeP until DATa
+ * 0x25 addr
+ * sleep until $addr, then carry on from current PC
+ * Note: The value from $addr is considered to be a block height. + */ + SLP_DAT(0x25, OpCodeParam.BLOCK_HEIGHT) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + int address = Utils.getCodeAddress(codeByteBuffer); + long value = codeByteBuffer.getLong(address); + + state.sleepUntilHeight = (int) value; + state.isSleeping = true; + } + }, + /** + * FInish if Zero DATa
+ * 0x26 addr
+ * if ($addr == 0) permanently stop + */ + FIZ_DAT(0x26, OpCodeParam.SRC_ADDR) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + int address = Utils.getDataAddress(codeByteBuffer, dataByteBuffer); + long value = dataByteBuffer.getLong(address); + + if (value == 0) + state.isFinished = true; + } + }, + /** + * STop if Zero DATa
+ * 0x27 addr
+ * if ($addr == 0) PC = PCS and stop + */ + STZ_DAT(0x27, OpCodeParam.SRC_ADDR) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + int address = Utils.getDataAddress(codeByteBuffer, dataByteBuffer); + long value = dataByteBuffer.getLong(address); + + if (value == 0) { + state.programCounter = state.onStopAddress; + codeByteBuffer.position(state.onStopAddress); + state.isStopped = true; + } + } + }, + /** + * FINish IMmeDiately
+ * 0x28
+ * permanently stop + */ + FIN_IMD(0x28) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + state.isFinished = true; + } + }, + /** + * SToP IMmeDiately
+ * 0x29
+ * stop + */ + STP_IMD(0x29) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) { + state.isStopped = true; + } + }, + /** + * SLeeP IMmeDiately
+ * 0x2a
+ * sleep until next block, then carry on from current PC + */ + SLP_IMD(0x2a) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) { + state.sleepUntilHeight = state.currentBlockHeight + 1; + state.isSleeping = true; + } + }, + /** + * Set ERRor ADdRess
+ * 0x2b addr
+ * PCE = addr + */ + ERR_ADR(0x2b, OpCodeParam.CODE_ADDR) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + int address = Utils.getCodeAddress(codeByteBuffer); + + state.onErrorAddress = address; + } + }, + /** + * SET PCS (stop address)
+ * 0x30
+ * PCS = PC
+ * Note: PC is considered to be immediately after this opcode byte. + */ + SET_PCS(0x30) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) { + state.onStopAddress = codeByteBuffer.position(); + } + }, + /** + * Call EXTernal FUNction
+ * 0x32 func
+ * func() + */ + EXT_FUN(0x32, OpCodeParam.FUNC) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + short rawFunctionCode = codeByteBuffer.getShort(); + FunctionCode functionCode = FunctionCode.valueOf(rawFunctionCode); + + if (functionCode == null) + throw new IllegalFunctionCodeException("Unknown function code 0x" + String.format("%04x", rawFunctionCode) + " encountered at EXT_FUN"); + + functionCode.preExecuteCheck(0, false, state, rawFunctionCode); + + FunctionData functionData = new FunctionData(false); + + executeFunction(codeByteBuffer, functionCode, functionData, state, rawFunctionCode); + } + }, + /** + * Call EXTernal FUNction with DATa
+ * 0x33 func addr
+ * func($addr) + */ + EXT_FUN_DAT(0x33, OpCodeParam.FUNC, OpCodeParam.SRC_ADDR) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + short rawFunctionCode = codeByteBuffer.getShort(); + FunctionCode functionCode = FunctionCode.valueOf(rawFunctionCode); + + if (functionCode == null) + throw new IllegalFunctionCodeException("Unknown function code 0x" + String.format("%04x", rawFunctionCode) + " encountered at EXT_FUN_DAT"); + + functionCode.preExecuteCheck(1, false, state, rawFunctionCode); + + int address = Utils.getDataAddress(codeByteBuffer, dataByteBuffer); + long value = dataByteBuffer.getLong(address); + + FunctionData functionData = new FunctionData(value, false); + + executeFunction(codeByteBuffer, functionCode, functionData, state, rawFunctionCode); + } + }, + /** + * Call EXTernal FUNction with DATa x2
+ * 0x34 func addr1 addr2
+ * func($addr1, $addr2) + */ + EXT_FUN_DAT_2(0x34, OpCodeParam.FUNC, OpCodeParam.SRC_ADDR, OpCodeParam.SRC_ADDR) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + short rawFunctionCode = codeByteBuffer.getShort(); + FunctionCode functionCode = FunctionCode.valueOf(rawFunctionCode); + + if (functionCode == null) + throw new IllegalFunctionCodeException("Unknown function code 0x" + String.format("%04x", rawFunctionCode) + " encountered at EXT_FUN_DAT_2"); + + functionCode.preExecuteCheck(2, false, state, rawFunctionCode); + + int address1 = Utils.getDataAddress(codeByteBuffer, dataByteBuffer); + int address2 = Utils.getDataAddress(codeByteBuffer, dataByteBuffer); + + long value1 = dataByteBuffer.getLong(address1); + long value2 = dataByteBuffer.getLong(address2); + + FunctionData functionData = new FunctionData(value1, value2, false); + + executeFunction(codeByteBuffer, functionCode, functionData, state, rawFunctionCode); + } + }, + /** + * Call EXTernal FUNction expecting RETurn value
+ * 0x35 func addr
+ * @addr = func() + */ + EXT_FUN_RET(0x35, OpCodeParam.FUNC, OpCodeParam.DEST_ADDR) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + short rawFunctionCode = codeByteBuffer.getShort(); + FunctionCode functionCode = FunctionCode.valueOf(rawFunctionCode); + + if (functionCode == null) + throw new IllegalFunctionCodeException("Unknown function code 0x" + String.format("%04x", rawFunctionCode) + " encountered at EXT_FUN_RET"); + + functionCode.preExecuteCheck(0, true, state, rawFunctionCode); + + int address = Utils.getDataAddress(codeByteBuffer, dataByteBuffer); + + FunctionData functionData = new FunctionData(true); + + executeFunction(codeByteBuffer, functionCode, functionData, state, rawFunctionCode); + + if (functionData.returnValue == null) + throw new ExecutionException("Function failed to return a value as expected of EXT_FUN_RET"); + + dataByteBuffer.putLong(address, functionData.returnValue); + } + }, + /** + * Call EXTernal FUNction expecting RETurn value with DATa
+ * 0x36 func addr1 addr2
+ * @addr1 = func($addr2) + */ + EXT_FUN_RET_DAT(0x36, OpCodeParam.FUNC, OpCodeParam.DEST_ADDR, OpCodeParam.SRC_ADDR) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + short rawFunctionCode = codeByteBuffer.getShort(); + FunctionCode functionCode = FunctionCode.valueOf(rawFunctionCode); + + if (functionCode == null) + throw new IllegalFunctionCodeException("Unknown function code 0x" + String.format("%04x", rawFunctionCode) + " encountered at EXT_FUN_RET_DAT"); + + functionCode.preExecuteCheck(1, true, state, rawFunctionCode); + + int address1 = Utils.getDataAddress(codeByteBuffer, dataByteBuffer); + int address2 = Utils.getDataAddress(codeByteBuffer, dataByteBuffer); + + long value = dataByteBuffer.getLong(address2); + + FunctionData functionData = new FunctionData(value, true); + + executeFunction(codeByteBuffer, functionCode, functionData, state, rawFunctionCode); + + if (functionData.returnValue == null) + throw new ExecutionException("Function failed to return a value as expected of EXT_FUN_RET_DAT"); + + dataByteBuffer.putLong(address1, functionData.returnValue); + } + }, + /** + * Call EXTernal FUNction expecting RETurn value with DATa x2
+ * 0x37 func addr1 addr2 addr3
+ * @addr1 = func($addr2, $addr3) + */ + EXT_FUN_RET_DAT_2(0x37, OpCodeParam.FUNC, OpCodeParam.DEST_ADDR, OpCodeParam.SRC_ADDR, OpCodeParam.SRC_ADDR) { + @Override + public void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException { + short rawFunctionCode = codeByteBuffer.getShort(); + FunctionCode functionCode = FunctionCode.valueOf(rawFunctionCode); + + if (functionCode == null) + throw new IllegalFunctionCodeException( + "Unknown function code 0x" + String.format("%04x", rawFunctionCode) + " encountered at EXT_FUN_RET_DAT_2"); + + functionCode.preExecuteCheck(2, true, state, rawFunctionCode); + + int address1 = Utils.getDataAddress(codeByteBuffer, dataByteBuffer); + int address2 = Utils.getDataAddress(codeByteBuffer, dataByteBuffer); + int address3 = Utils.getDataAddress(codeByteBuffer, dataByteBuffer); + + long value1 = dataByteBuffer.getLong(address2); + long value2 = dataByteBuffer.getLong(address3); + + FunctionData functionData = new FunctionData(value1, value2, true); + + executeFunction(codeByteBuffer, functionCode, functionData, state, rawFunctionCode); + + if (functionData.returnValue == null) + throw new ExecutionException("Function failed to return a value as expected of EXT_FUN_RET_DAT_2"); + + dataByteBuffer.putLong(address1, functionData.returnValue); + } + }; + + public final byte value; + public final OpCodeParam[] params; + + // Create a map of opcode values to OpCode + private final static Map map = Arrays.stream(OpCode.values()).collect(Collectors.toMap(opcode -> opcode.value, opcode -> opcode)); + + private OpCode(int value, OpCodeParam... params) { + this.value = (byte) value; + this.params = params; + } + + public static OpCode valueOf(int value) { + return map.get((byte) value); + } + + /** + * Execute OpCode + *

+ * Assumes codeByteBuffer.position() is already placed immediately after opcode. + *

+ * Updates codeByteBuffer.position() as arguments are fetched, so caller should update state.programCounter using + * codeByteBuffer.position() on return. + *

+ * Can also modify userStackByteBuffer and various fields of state. + *

+ * Throws a subclass of ExecutionException on error, e.g. InvalidAddressException. + * + * @param codeByteBuffer + * @param dataByteBuffer + * @param userStackByteBuffer + * @param callStackByteBuffer + * @param state + * @throws ExecutionException + */ + public abstract void execute(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, ByteBuffer userStackByteBuffer, ByteBuffer callStackByteBuffer, + MachineState state) throws ExecutionException; + + /** + * Returns string representing disassembled OpCode and parameters + * + * @param codeByteBuffer + * @param dataByteBuffer + * @return String + * @throws ExecutionException + */ + public String disassemble(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer) throws ExecutionException { + String output = this.name(); + + int postOpcodeProgramCounter = codeByteBuffer.position(); + + for (OpCodeParam param : this.params) { + output += " " + param.disassemble(codeByteBuffer, dataByteBuffer, postOpcodeProgramCounter); + } + + return output; + } + + /** + * Common code for ADD_DAT/SUB_DAT/MUL_DAT/DIV_DAT/MOD_DAT/SHL_DAT/SHR_DAT + * + * @param codeByteBuffer + * @param dataByteBuffer + * @param operator + * - typically a lambda operating on two long params, e.g. (a, b) -> a + b + * @throws ExecutionException + */ + private static void executeDataOperation(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, TwoValueOperator operator) throws ExecutionException { + int address1 = Utils.getDataAddress(codeByteBuffer, dataByteBuffer); + int address2 = Utils.getDataAddress(codeByteBuffer, dataByteBuffer); + + long value1 = dataByteBuffer.getLong(address1); + long value2 = dataByteBuffer.getLong(address2); + + long newValue = operator.apply(value1, value2); + + dataByteBuffer.putLong(address1, newValue); + } + + /** + * Common code for BGT/BLT/BGE/BLE/BEQ/BNE + * + * @param codeByteBuffer + * @param dataByteBuffer + * @param state + * @param comparator + * - typically a lambda comparing two long params, e.g. (a, b) -> a == b + * @throws ExecutionException + */ + private static void executeBranchConditional(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, MachineState state, TwoValueComparator comparator) + throws ExecutionException { + int opCodePosition = codeByteBuffer.position() - 1; // i.e. before this OpCode + + int address1 = Utils.getDataAddress(codeByteBuffer, dataByteBuffer); + int address2 = Utils.getDataAddress(codeByteBuffer, dataByteBuffer); + byte offset = Utils.getCodeOffset(codeByteBuffer); + + int branchTarget = opCodePosition + offset; + + long value1 = dataByteBuffer.getLong(address1); + long value2 = dataByteBuffer.getLong(address2); + + if (comparator.compare(value1, value2)) + codeByteBuffer.position(branchTarget); + } + + /** + * Common code for executing a function. + *

+ * Updates programCounter to codeByteBuffer's position before calling function.
+ * Adjusts codeByteBuffer position to programCounter after calling function. + *

+ * This is needed for functions that might use/alter the programCounter during their execution,
+ * for example {@link FunctionCode#GENERATE_RANDOM_USING_TX_IN_A} + * + * @see FunctionCode#GENERATE_RANDOM_USING_TX_IN_A + * + * @param codeByteBuffer + * @param functionCode + * @param functionData + * @param state + * @throws ExecutionException + */ + private static void executeFunction(ByteBuffer codeByteBuffer, FunctionCode functionCode, FunctionData functionData, MachineState state, + short rawFunctionCode) throws ExecutionException { + state.programCounter = codeByteBuffer.position(); + + functionCode.execute(functionData, state, rawFunctionCode); + + codeByteBuffer.position(state.programCounter); + } + +} diff --git a/Java/src/org/ciyam/at/OpCodeParam.java b/Java/src/org/ciyam/at/OpCodeParam.java new file mode 100644 index 0000000..00b0e75 --- /dev/null +++ b/Java/src/org/ciyam/at/OpCodeParam.java @@ -0,0 +1,160 @@ +package org.ciyam.at; + +import java.nio.ByteBuffer; + +public enum OpCodeParam { + + VALUE { + @Override + public Object fetch(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer) throws ExecutionException { + return new Long(Utils.getCodeValue(codeByteBuffer)); + } + + @Override + protected String toString(Object value, int postOpcodeProgramCounter) { + return String.format("#%016x", (Long) value); + } + }, + DEST_ADDR { + @Override + public Object fetch(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer) throws ExecutionException { + return new Integer(Utils.getDataAddress(codeByteBuffer, dataByteBuffer)); + } + + @Override + protected String toString(Object value, int postOpcodeProgramCounter) { + return String.format("@%08x", ((Integer) value) / MachineState.VALUE_SIZE); + } + }, + INDIRECT_DEST_ADDR { + @Override + public Object fetch(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer) throws ExecutionException { + return new Integer(Utils.getDataAddress(codeByteBuffer, dataByteBuffer)); + } + + @Override + protected String toString(Object value, int postOpcodeProgramCounter) { + return String.format("@($%08x)", ((Integer) value) / MachineState.VALUE_SIZE); + } + }, + INDIRECT_DEST_ADDR_WITH_INDEX { + @Override + public Object fetch(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer) throws ExecutionException { + return new Integer(Utils.getDataAddress(codeByteBuffer, dataByteBuffer)); + } + + @Override + protected String toString(Object value, int postOpcodeProgramCounter) { + return String.format("@($%08x", ((Integer) value) / MachineState.VALUE_SIZE); + } + }, + SRC_ADDR { + @Override + public Object fetch(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer) throws ExecutionException { + return new Integer(Utils.getDataAddress(codeByteBuffer, dataByteBuffer)); + } + + @Override + protected String toString(Object value, int postOpcodeProgramCounter) { + return String.format("$%08x", ((Integer) value) / MachineState.VALUE_SIZE); + } + }, + INDIRECT_SRC_ADDR { + @Override + public Object fetch(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer) throws ExecutionException { + return new Integer(Utils.getDataAddress(codeByteBuffer, dataByteBuffer)); + } + + @Override + protected String toString(Object value, int postOpcodeProgramCounter) { + return String.format("$($%08x)", ((Integer) value) / MachineState.VALUE_SIZE); + } + }, + INDIRECT_SRC_ADDR_WITH_INDEX { + @Override + public Object fetch(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer) throws ExecutionException { + return new Integer(Utils.getDataAddress(codeByteBuffer, dataByteBuffer)); + } + + @Override + protected String toString(Object value, int postOpcodeProgramCounter) { + return String.format("$($%08x", ((Integer) value) / MachineState.VALUE_SIZE); + } + }, + INDEX { + @Override + public Object fetch(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer) throws ExecutionException { + return new Integer(Utils.getDataAddress(codeByteBuffer, dataByteBuffer)); + } + + @Override + protected String toString(Object value, int postOpcodeProgramCounter) { + return String.format("+ $%08x)", ((Integer) value) / MachineState.VALUE_SIZE); + } + }, + CODE_ADDR { + @Override + public Object fetch(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer) throws ExecutionException { + return new Integer(Utils.getCodeAddress(codeByteBuffer)); + } + + @Override + protected String toString(Object value, int postOpcodeProgramCounter) { + return String.format("[%04x]", (Integer) value); + } + }, + OFFSET { + @Override + public Object fetch(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer) throws ExecutionException { + return new Byte(Utils.getCodeOffset(codeByteBuffer)); + } + + @Override + protected String toString(Object value, int postOpcodeProgramCounter) { + return String.format("PC+%02x=[%04x]", (int) ((Byte) value), postOpcodeProgramCounter - 1 + (Byte) value); + } + }, + FUNC { + @Override + public Object fetch(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer) throws ExecutionException { + return new Short(codeByteBuffer.getShort()); + } + + @Override + protected String toString(Object value, int postOpcodeProgramCounter) { + FunctionCode functionCode = FunctionCode.valueOf((Short) value); + + // generic/unknown form + if (functionCode == null) + return String.format("FN(%04x)", (Short) value); + + // API pass-through + if (functionCode == FunctionCode.API_PASSTHROUGH) + return String.format("API-FN(%04x)", (Short) value); + + return "\"" + functionCode.name() + "\"" + String.format("{%04x}", (Short) value); + } + }, + BLOCK_HEIGHT { + @Override + public Object fetch(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer) throws ExecutionException { + return new Integer(codeByteBuffer.getInt()); + } + + @Override + protected String toString(Object value, int postOpcodeProgramCounter) { + return String.format("height $%08x", ((Integer) value) / MachineState.VALUE_SIZE); + } + }; + + public abstract Object fetch(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer) throws ExecutionException; + + public String disassemble(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, int postOpcodeProgramCounter) throws ExecutionException { + Object value = fetch(codeByteBuffer, dataByteBuffer); + + return this.toString(value, postOpcodeProgramCounter); + } + + protected abstract String toString(Object value, int postOpcodeProgramCounter); + +} diff --git a/Java/src/org/ciyam/at/StackBoundsException.java b/Java/src/org/ciyam/at/StackBoundsException.java new file mode 100644 index 0000000..8c394e9 --- /dev/null +++ b/Java/src/org/ciyam/at/StackBoundsException.java @@ -0,0 +1,21 @@ +package org.ciyam.at; + +@SuppressWarnings("serial") +public class StackBoundsException extends ExecutionException { + + public StackBoundsException() { + } + + public StackBoundsException(String message) { + super(message); + } + + public StackBoundsException(Throwable cause) { + super(cause); + } + + public StackBoundsException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/Java/src/org/ciyam/at/Timestamp.java b/Java/src/org/ciyam/at/Timestamp.java new file mode 100644 index 0000000..55f6064 --- /dev/null +++ b/Java/src/org/ciyam/at/Timestamp.java @@ -0,0 +1,66 @@ +package org.ciyam.at; + +/** + * CIYAM-AT "Timestamp" + *

+ * With CIYAM-ATs, "timestamp" does not mean a real timestamp but instead is an artificial timestamp that includes two parts. The first part is a block height + * (32 bits) with the second part being the number of the transaction if applicable (also 32 bits and zero if not applicable). Timestamps can thus be + * represented as a 64 bit long. + *

+ * + * @see Timestamp#Timestamp(int, int) + * @see Timestamp#Timestamp(long) + * @see Timestamp#longValue() + * @see Timestamp#toLong(int, int) + * + */ +public class Timestamp { + + public int blockHeight; + public int transactionSequence; + + /** + * Constructs new CIYAM-AT "timestamp" using block height and transaction sequence. + * + * @param blockHeight + * @param transactionSequence + */ + public Timestamp(int blockHeight, int transactionSequence) { + this.blockHeight = blockHeight; + this.transactionSequence = transactionSequence; + } + + /** + * Constructs new CIYAM-AT "timestamp" using long packed with block height and transaction sequence. + * + * @param timestamp + */ + public Timestamp(long timestamp) { + this.blockHeight = (int) (timestamp >> 32); + this.transactionSequence = (int) (timestamp & 0xffffff); + } + + /** + * Returns CIYAM-AT "timestamp" long representing block height and transaction sequence. + * + * @return CIYAM-AT "timestamp" as long + */ + public long longValue() { + return Timestamp.toLong(this.blockHeight, this.transactionSequence); + } + + /** + * Returns CIYAM-AT "timestamp" long representing block height and transaction sequence. + * + * @param blockHeight + * @param transactionSequence + * @return CIYAM-AT "timestamp" as long + */ + public static long toLong(int blockHeight, int transactionSequence) { + long longValue = blockHeight; + longValue <<= 32; + longValue |= transactionSequence; + return longValue; + } + +} diff --git a/Java/src/org/ciyam/at/TwoValueComparator.java b/Java/src/org/ciyam/at/TwoValueComparator.java new file mode 100644 index 0000000..00c6430 --- /dev/null +++ b/Java/src/org/ciyam/at/TwoValueComparator.java @@ -0,0 +1,7 @@ +package org.ciyam.at; + +public interface TwoValueComparator { + + public boolean compare(long a, long b); + +} diff --git a/Java/src/org/ciyam/at/TwoValueOperator.java b/Java/src/org/ciyam/at/TwoValueOperator.java new file mode 100644 index 0000000..4cc6a55 --- /dev/null +++ b/Java/src/org/ciyam/at/TwoValueOperator.java @@ -0,0 +1,7 @@ +package org.ciyam.at; + +public interface TwoValueOperator { + + public long apply(long a, long b); + +} diff --git a/Java/src/org/ciyam/at/Utils.java b/Java/src/org/ciyam/at/Utils.java new file mode 100644 index 0000000..42e3dba --- /dev/null +++ b/Java/src/org/ciyam/at/Utils.java @@ -0,0 +1,126 @@ +package org.ciyam.at; + +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; + +public class Utils { + + /** + * Returns immediate function code enum from code bytes at current position. + *

+ * Initial position is codeByteBuffer.position() but on return is incremented by 2. + * + * @param codeByteBuffer + * @return FunctionCode enum + * @throws CodeSegmentException + * @throws InvalidAddressException + */ + public static FunctionCode getFunctionCode(ByteBuffer codeByteBuffer) throws CodeSegmentException, IllegalFunctionCodeException { + try { + int rawFunctionCode = codeByteBuffer.getShort(); + + FunctionCode functionCode = FunctionCode.valueOf(rawFunctionCode); + + if (functionCode == null) + throw new IllegalFunctionCodeException("Unknown function code"); + + return functionCode; + } catch (BufferUnderflowException e) { + throw new CodeSegmentException("No code bytes left to get function code", e); + } + } + + /** + * Returns code address from code bytes at current position. + *

+ * Initial position is codeByteBuffer.position() but on return is incremented by 4. + *

+ * Note: address is not scaled by Constants.VALUE_SIZE unlike other methods in this class. + * + * @param codeByteBuffer + * @return int address into code bytes + * @throws CodeSegmentException + * @throws InvalidAddressException + */ + public static int getCodeAddress(ByteBuffer codeByteBuffer) throws CodeSegmentException, InvalidAddressException { + try { + int address = codeByteBuffer.getInt(); + + if (address < 0 || address > MachineState.MAX_CODE_ADDRESS || address >= codeByteBuffer.limit()) + throw new InvalidAddressException("Code address out of bounds"); + + return address; + } catch (BufferUnderflowException e) { + throw new CodeSegmentException("No code bytes left to get code address", e); + } + } + + /** + * Returns data address from code bytes at current position. + *

+ * Initial position is codeByteBuffer.position() but on return is incremented by 4. + *

+ * Note: address is returned scaled by Constants.VALUE_SIZE. + * + * @param codeByteBuffer + * @return int address into data bytes + * @throws CodeSegmentException + * @throws InvalidAddressException + */ + public static int getDataAddress(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer) throws CodeSegmentException, InvalidAddressException { + try { + int address = codeByteBuffer.getInt() * MachineState.VALUE_SIZE; + + if (address < 0 || address + MachineState.VALUE_SIZE >= dataByteBuffer.limit()) + throw new InvalidAddressException("Data address out of bounds"); + + return address; + } catch (BufferUnderflowException e) { + throw new CodeSegmentException("No code bytes left to get data address", e); + } + } + + /** + * Returns byte offset from code bytes at current position. + *

+ * Initial position is codeByteBuffer.position() but on return is incremented by 1. + *

+ * Note: offset is not scaled by Constants.VALUE_SIZE unlike other methods in this class. + * + * @param codeByteBuffer + * @return byte offset + * @throws CodeSegmentException + * @throws InvalidAddressException + */ + public static byte getCodeOffset(ByteBuffer codeByteBuffer) throws CodeSegmentException, InvalidAddressException { + try { + byte offset = codeByteBuffer.get(); + + if (codeByteBuffer.position() + offset < 0 || codeByteBuffer.position() + offset >= codeByteBuffer.limit()) + throw new InvalidAddressException("Code offset out of bounds"); + + return offset; + } catch (BufferUnderflowException e) { + throw new CodeSegmentException("No code bytes left to get code offset", e); + } + } + + /** + * Returns long immediate value from code bytes at current position. + *

+ * Initial position is codeByteBuffer.position() but on return is incremented by 8. + * + * @param codeByteBuffer + * @return long value + * @throws CodeSegmentException + * @throws InvalidAddressException + */ + public static long getCodeValue(ByteBuffer codeByteBuffer) throws CodeSegmentException, InvalidAddressException { + try { + return codeByteBuffer.getLong(); + } catch (BufferUnderflowException e) { + throw new CodeSegmentException("No code bytes left to get immediate value", e); + } + } + +} diff --git a/Java/tests/BranchingOpCodeTests.java b/Java/tests/BranchingOpCodeTests.java new file mode 100644 index 0000000..1f03606 --- /dev/null +++ b/Java/tests/BranchingOpCodeTests.java @@ -0,0 +1,496 @@ +import static common.TestUtils.hexToBytes; +import static org.junit.Assert.*; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.Security; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.ciyam.at.API; +import org.ciyam.at.ExecutionException; +import org.ciyam.at.MachineState; +import org.ciyam.at.OpCode; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import common.TestAPI; +import common.TestLogger; + +public class BranchingOpCodeTests { + + public TestLogger logger; + public API api; + public MachineState state; + public ByteBuffer codeByteBuffer; + + @BeforeClass + public static void beforeClass() { + Security.insertProviderAt(new BouncyCastleProvider(), 0); + } + + @Before + public void beforeTest() { + logger = new TestLogger(); + api = new TestAPI(); + codeByteBuffer = ByteBuffer.allocate(512).order(ByteOrder.LITTLE_ENDIAN); + } + + @After + public void afterTest() { + codeByteBuffer = null; + api = null; + logger = null; + } + + private void execute() { + System.out.println("Starting execution:"); + System.out.println("Current block height: " + state.currentBlockHeight); + + state.execute(); + + System.out.println("After execution:"); + System.out.println("Steps: " + state.steps); + System.out.println("Program Counter: " + String.format("%04x", state.programCounter)); + System.out.println("Stop Address: " + String.format("%04x", state.onStopAddress)); + System.out.println("Error Address: " + (state.onErrorAddress == null ? "not set" : String.format("%04x", state.onErrorAddress))); + if (state.isSleeping) + System.out.println("Sleeping until current block height (" + state.currentBlockHeight + ") reaches " + state.sleepUntilHeight); + else + System.out.println("Sleeping: " + state.isSleeping); + System.out.println("Stopped: " + state.isStopped); + System.out.println("Finished: " + state.isFinished); + if (state.hadFatalError) + System.out.println("Finished due to fatal error!"); + System.out.println("Frozen: " + state.isFrozen); + } + + private void simulate() { + // version 0003, reserved 0000, code 0200 * 1, data 0020 * 8, call stack 0010 * 4, user stack 0010 * 8 + byte[] headerBytes = hexToBytes("0300" + "0000" + "0002" + "2000" + "1000" + "1000"); + byte[] codeBytes = codeByteBuffer.array(); + byte[] dataBytes = new byte[0]; + + state = new MachineState(api, logger, headerBytes, codeBytes, dataBytes); + + do { + execute(); + + // Bump block height + state.currentBlockHeight++; + } while (!state.isFinished); + + } + + @Test + public void testBZR_DATtrue() throws ExecutionException { + int targetAddr = 0x21; + + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(0L); + int tempPC = codeByteBuffer.position(); + codeByteBuffer.put(OpCode.BZR_DAT.value).putInt(0).put((byte) (targetAddr - tempPC)); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + // targetAddr: + assertEquals(targetAddr, codeByteBuffer.position()); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(2L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", 2L, state.dataByteBuffer.getLong(1 * MachineState.VALUE_SIZE)); + } + + @Test + public void testBZR_DATfalse() throws ExecutionException { + int targetAddr = 0x21; + + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(9999L); + int tempPC = codeByteBuffer.position(); + codeByteBuffer.put(OpCode.BZR_DAT.value).putInt(0).put((byte) (targetAddr - tempPC)); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + // targetAddr: + assertEquals(targetAddr, codeByteBuffer.position()); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(2L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", 1L, state.dataByteBuffer.getLong(1 * MachineState.VALUE_SIZE)); + } + + @Test + public void testBNZ_DATtrue() throws ExecutionException { + int targetAddr = 0x21; + + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(9999L); + int tempPC = codeByteBuffer.position(); + codeByteBuffer.put(OpCode.BNZ_DAT.value).putInt(0).put((byte) (targetAddr - tempPC)); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + // targetAddr: + assertEquals(targetAddr, codeByteBuffer.position()); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(2L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", 2L, state.dataByteBuffer.getLong(1 * MachineState.VALUE_SIZE)); + } + + @Test + public void testBNZ_DATfalse() throws ExecutionException { + int targetAddr = 0x21; + + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(0L); + int tempPC = codeByteBuffer.position(); + codeByteBuffer.put(OpCode.BNZ_DAT.value).putInt(0).put((byte) (targetAddr - tempPC)); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + // targetAddr: + assertEquals(targetAddr, codeByteBuffer.position()); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(2L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", 1L, state.dataByteBuffer.getLong(1 * MachineState.VALUE_SIZE)); + } + + @Test + public void testBGT_DATtrue() throws ExecutionException { + int targetAddr = 0x32; + + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(2222L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1111L); + int tempPC = codeByteBuffer.position(); + codeByteBuffer.put(OpCode.BGT_DAT.value).putInt(0).putInt(1).put((byte) (targetAddr - tempPC)); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(1L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + // targetAddr: + assertEquals(targetAddr, codeByteBuffer.position()); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", 2L, state.dataByteBuffer.getLong(2 * MachineState.VALUE_SIZE)); + } + + @Test + public void testBGT_DATfalse() throws ExecutionException { + int targetAddr = 0x32; + + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(1111L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(2222L); + int tempPC = codeByteBuffer.position(); + codeByteBuffer.put(OpCode.BGT_DAT.value).putInt(0).putInt(1).put((byte) (targetAddr - tempPC)); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(1L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + // targetAddr: + assertEquals(targetAddr, codeByteBuffer.position()); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", 1L, state.dataByteBuffer.getLong(2 * MachineState.VALUE_SIZE)); + } + + @Test + public void testBLT_DATtrue() throws ExecutionException { + int targetAddr = 0x32; + + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(1111L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(2222L); + int tempPC = codeByteBuffer.position(); + codeByteBuffer.put(OpCode.BLT_DAT.value).putInt(0).putInt(1).put((byte) (targetAddr - tempPC)); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(1L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + // targetAddr: + assertEquals(targetAddr, codeByteBuffer.position()); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", 2L, state.dataByteBuffer.getLong(2 * MachineState.VALUE_SIZE)); + } + + @Test + public void testBLT_DATfalse() throws ExecutionException { + int targetAddr = 0x32; + + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(2222L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1111L); + int tempPC = codeByteBuffer.position(); + codeByteBuffer.put(OpCode.BLT_DAT.value).putInt(0).putInt(1).put((byte) (targetAddr - tempPC)); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(1L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + // targetAddr: + assertEquals(targetAddr, codeByteBuffer.position()); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", 1L, state.dataByteBuffer.getLong(2 * MachineState.VALUE_SIZE)); + } + + @Test + public void testBGE_DATtrue1() throws ExecutionException { + int targetAddr = 0x32; + + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(2222L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1111L); + int tempPC = codeByteBuffer.position(); + codeByteBuffer.put(OpCode.BGE_DAT.value).putInt(0).putInt(1).put((byte) (targetAddr - tempPC)); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(1L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + // targetAddr: + assertEquals(targetAddr, codeByteBuffer.position()); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", 2L, state.dataByteBuffer.getLong(2 * MachineState.VALUE_SIZE)); + } + + @Test + public void testBGE_DATtrue2() throws ExecutionException { + int targetAddr = 0x32; + + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(2222L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(2222L); + int tempPC = codeByteBuffer.position(); + codeByteBuffer.put(OpCode.BGE_DAT.value).putInt(0).putInt(1).put((byte) (targetAddr - tempPC)); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(1L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + // targetAddr: + assertEquals(targetAddr, codeByteBuffer.position()); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", 2L, state.dataByteBuffer.getLong(2 * MachineState.VALUE_SIZE)); + } + + @Test + public void testBGE_DATfalse() throws ExecutionException { + int targetAddr = 0x32; + + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(1111L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(2222L); + int tempPC = codeByteBuffer.position(); + codeByteBuffer.put(OpCode.BGE_DAT.value).putInt(0).putInt(1).put((byte) (targetAddr - tempPC)); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(1L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + // targetAddr: + assertEquals(targetAddr, codeByteBuffer.position()); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", 1L, state.dataByteBuffer.getLong(2 * MachineState.VALUE_SIZE)); + } + + @Test + public void testBLE_DATtrue1() throws ExecutionException { + int targetAddr = 0x32; + + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(1111L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(2222L); + int tempPC = codeByteBuffer.position(); + codeByteBuffer.put(OpCode.BLE_DAT.value).putInt(0).putInt(1).put((byte) (targetAddr - tempPC)); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(1L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + // targetAddr: + assertEquals(targetAddr, codeByteBuffer.position()); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", 2L, state.dataByteBuffer.getLong(2 * MachineState.VALUE_SIZE)); + } + + @Test + public void testBLE_DATtrue2() throws ExecutionException { + int targetAddr = 0x32; + + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(2222L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(2222L); + int tempPC = codeByteBuffer.position(); + codeByteBuffer.put(OpCode.BLE_DAT.value).putInt(0).putInt(1).put((byte) (targetAddr - tempPC)); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(1L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + // targetAddr: + assertEquals(targetAddr, codeByteBuffer.position()); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", 2L, state.dataByteBuffer.getLong(2 * MachineState.VALUE_SIZE)); + } + + @Test + public void testBLE_DATfalse() throws ExecutionException { + int targetAddr = 0x32; + + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(2222L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1111L); + int tempPC = codeByteBuffer.position(); + codeByteBuffer.put(OpCode.BLE_DAT.value).putInt(0).putInt(1).put((byte) (targetAddr - tempPC)); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(1L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + // targetAddr: + assertEquals(targetAddr, codeByteBuffer.position()); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", 1L, state.dataByteBuffer.getLong(2 * MachineState.VALUE_SIZE)); + } + + @Test + public void testBEQ_DATtrue() throws ExecutionException { + int targetAddr = 0x32; + + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(2222L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(2222L); + int tempPC = codeByteBuffer.position(); + codeByteBuffer.put(OpCode.BEQ_DAT.value).putInt(0).putInt(1).put((byte) (targetAddr - tempPC)); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(1L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + // targetAddr: + assertEquals(targetAddr, codeByteBuffer.position()); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", 2L, state.dataByteBuffer.getLong(2 * MachineState.VALUE_SIZE)); + } + + @Test + public void testBEQ_DATfalse() throws ExecutionException { + int targetAddr = 0x32; + + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(1111L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(2222L); + int tempPC = codeByteBuffer.position(); + codeByteBuffer.put(OpCode.BEQ_DAT.value).putInt(0).putInt(1).put((byte) (targetAddr - tempPC)); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(1L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + // targetAddr: + assertEquals(targetAddr, codeByteBuffer.position()); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", 1L, state.dataByteBuffer.getLong(2 * MachineState.VALUE_SIZE)); + } + + @Test + public void testBNE_DATtrue() throws ExecutionException { + int targetAddr = 0x32; + + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(2222L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1111L); + int tempPC = codeByteBuffer.position(); + codeByteBuffer.put(OpCode.BNE_DAT.value).putInt(0).putInt(1).put((byte) (targetAddr - tempPC)); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(1L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + // targetAddr: + assertEquals(targetAddr, codeByteBuffer.position()); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", 2L, state.dataByteBuffer.getLong(2 * MachineState.VALUE_SIZE)); + } + + @Test + public void testBNE_DATfalse() throws ExecutionException { + int targetAddr = 0x32; + + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(2222L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(2222L); + int tempPC = codeByteBuffer.position(); + codeByteBuffer.put(OpCode.BNE_DAT.value).putInt(0).putInt(1).put((byte) (targetAddr - tempPC)); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(1L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + // targetAddr: + assertEquals(targetAddr, codeByteBuffer.position()); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", 1L, state.dataByteBuffer.getLong(2 * MachineState.VALUE_SIZE)); + } + +} diff --git a/Java/tests/CallStackOpCodeTests.java b/Java/tests/CallStackOpCodeTests.java new file mode 100644 index 0000000..d4afa8c --- /dev/null +++ b/Java/tests/CallStackOpCodeTests.java @@ -0,0 +1,255 @@ +import static common.TestUtils.hexToBytes; +import static org.junit.Assert.*; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.Security; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.ciyam.at.API; +import org.ciyam.at.ExecutionException; +import org.ciyam.at.MachineState; +import org.ciyam.at.OpCode; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import common.TestAPI; +import common.TestLogger; + +public class CallStackOpCodeTests { + + public TestLogger logger; + public API api; + public MachineState state; + public ByteBuffer codeByteBuffer; + + @BeforeClass + public static void beforeClass() { + Security.insertProviderAt(new BouncyCastleProvider(), 0); + } + + @Before + public void beforeTest() { + logger = new TestLogger(); + api = new TestAPI(); + codeByteBuffer = ByteBuffer.allocate(512).order(ByteOrder.LITTLE_ENDIAN); + } + + @After + public void afterTest() { + codeByteBuffer = null; + api = null; + logger = null; + } + + private void execute() { + System.out.println("Starting execution:"); + System.out.println("Current block height: " + state.currentBlockHeight); + + state.execute(); + + System.out.println("After execution:"); + System.out.println("Steps: " + state.steps); + System.out.println("Program Counter: " + String.format("%04x", state.programCounter)); + System.out.println("Stop Address: " + String.format("%04x", state.onStopAddress)); + System.out.println("Error Address: " + (state.onErrorAddress == null ? "not set" : String.format("%04x", state.onErrorAddress))); + if (state.isSleeping) + System.out.println("Sleeping until current block height (" + state.currentBlockHeight + ") reaches " + state.sleepUntilHeight); + else + System.out.println("Sleeping: " + state.isSleeping); + System.out.println("Stopped: " + state.isStopped); + System.out.println("Finished: " + state.isFinished); + if (state.hadFatalError) + System.out.println("Finished due to fatal error!"); + System.out.println("Frozen: " + state.isFrozen); + } + + private void simulate() { + // version 0003, reserved 0000, code 0200 * 1, data 0020 * 8, call stack 0010 * 4, user stack 0010 * 8 + byte[] headerBytes = hexToBytes("0300" + "0000" + "0002" + "2000" + "1000" + "1000"); + byte[] codeBytes = codeByteBuffer.array(); + byte[] dataBytes = new byte[0]; + + state = new MachineState(api, logger, headerBytes, codeBytes, dataBytes); + + do { + execute(); + + // Bump block height + state.currentBlockHeight++; + } while (!state.isFinished); + + } + + @Test + public void testJMP_SUB() throws ExecutionException { + int subAddr = 0x06; + + codeByteBuffer.put(OpCode.JMP_SUB.value).putInt(subAddr); + int returnAddress = codeByteBuffer.position(); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + // subAddr: + assertEquals(subAddr, codeByteBuffer.position()); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(4444L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + + int expectedCallStackPosition = (state.numCallStackPages - 1) * MachineState.CALL_STACK_PAGE_SIZE; + assertEquals("Call stack pointer incorrect", expectedCallStackPosition, state.callStackByteBuffer.position()); + + assertEquals("Return address does not match", returnAddress, state.callStackByteBuffer.getInt(expectedCallStackPosition)); + + assertEquals("Data does not match", 4444L, state.dataByteBuffer.getLong(0 * MachineState.VALUE_SIZE)); + } + + @Test + public void testJMP_SUB2() throws ExecutionException { + int subAddr1 = 0x06; + int subAddr2 = 0x19; + + codeByteBuffer.put(OpCode.JMP_SUB.value).putInt(subAddr1); + int returnAddress1 = codeByteBuffer.position(); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + // subAddr1: + assertEquals(subAddr1, codeByteBuffer.position()); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(4444L); + codeByteBuffer.put(OpCode.JMP_SUB.value).putInt(subAddr2); + int returnAddress2 = codeByteBuffer.position(); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + // subAddr2: + assertEquals(subAddr2, codeByteBuffer.position()); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(5555L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + + int expectedCallStackPosition = (state.numCallStackPages - 1 - 1) * MachineState.CALL_STACK_PAGE_SIZE; + assertEquals("Call stack pointer incorrect", expectedCallStackPosition, state.callStackByteBuffer.position()); + + assertEquals("Return address does not match", returnAddress2, state.callStackByteBuffer.getInt(expectedCallStackPosition)); + assertEquals("Return address does not match", returnAddress1, state.callStackByteBuffer.getInt(expectedCallStackPosition + MachineState.ADDRESS_SIZE)); + + assertEquals("Data does not match", 4444L, state.dataByteBuffer.getLong(0 * MachineState.VALUE_SIZE)); + assertEquals("Data does not match", 5555L, state.dataByteBuffer.getLong(1 * MachineState.VALUE_SIZE)); + } + + @Test + public void testJMP_SUBoverflow() throws ExecutionException { + // Call stack is 0x0010 entries in size, so exceed this to test overflow + for (int i = 0; i < 20; ++i) { + // sub address is next opcode! + codeByteBuffer.put(OpCode.JMP_SUB.value).putInt(i * (1 + 4)); + } + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertTrue(state.hadFatalError); + } + + @Test + public void testRET_SUB() throws ExecutionException { + int subAddr = 0x13; + + codeByteBuffer.put(OpCode.JMP_SUB.value).putInt(subAddr); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(7777L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + // subAddr: + assertEquals(subAddr, codeByteBuffer.position()); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(4444L); + codeByteBuffer.put(OpCode.RET_SUB.value); + codeByteBuffer.put(OpCode.FIN_IMD.value); // not reached! + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + + int expectedCallStackPosition = (state.numCallStackPages - 1 + 1) * MachineState.CALL_STACK_PAGE_SIZE; + assertEquals("Call stack pointer incorrect", expectedCallStackPosition, state.callStackByteBuffer.position()); + + assertEquals("Return address not cleared", 0L, state.callStackByteBuffer.getInt(expectedCallStackPosition - MachineState.ADDRESS_SIZE)); + + assertEquals("Data does not match", 4444L, state.dataByteBuffer.getLong(0 * MachineState.VALUE_SIZE)); + assertEquals("Data does not match", 7777L, state.dataByteBuffer.getLong(1 * MachineState.VALUE_SIZE)); + } + + @Test + public void testRET_SUB2() throws ExecutionException { + int subAddr1 = 0x13; + int subAddr2 = 0x34; + + codeByteBuffer.put(OpCode.JMP_SUB.value).putInt(subAddr1); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(7777L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + // subAddr1: + assertEquals(subAddr1, codeByteBuffer.position()); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(4444L); + codeByteBuffer.put(OpCode.JMP_SUB.value).putInt(subAddr2); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2222L); + codeByteBuffer.put(OpCode.RET_SUB.value); + codeByteBuffer.put(OpCode.FIN_IMD.value); // not reached! + + // subAddr2: + assertEquals(subAddr2, codeByteBuffer.position()); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(3).putLong(3333L); + codeByteBuffer.put(OpCode.RET_SUB.value); + codeByteBuffer.put(OpCode.FIN_IMD.value); // not reached! + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + + int expectedCallStackPosition = (state.numCallStackPages - 1 - 1 + 1 + 1) * MachineState.CALL_STACK_PAGE_SIZE; + assertEquals("Call stack pointer incorrect", expectedCallStackPosition, state.callStackByteBuffer.position()); + + assertEquals("Return address not cleared", 0L, state.callStackByteBuffer.getInt(expectedCallStackPosition - MachineState.ADDRESS_SIZE)); + + assertEquals("Data does not match", 4444L, state.dataByteBuffer.getLong(0 * MachineState.VALUE_SIZE)); + assertEquals("Data does not match", 7777L, state.dataByteBuffer.getLong(1 * MachineState.VALUE_SIZE)); + assertEquals("Data does not match", 2222L, state.dataByteBuffer.getLong(2 * MachineState.VALUE_SIZE)); + assertEquals("Data does not match", 3333L, state.dataByteBuffer.getLong(3 * MachineState.VALUE_SIZE)); + } + + @Test + public void testRET_SUBoverflow() throws ExecutionException { + codeByteBuffer.put(OpCode.RET_SUB.value); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertTrue(state.hadFatalError); + } + + @Test + public void testRET_SUBoverflow2() throws ExecutionException { + // sub address is next opcode! + codeByteBuffer.put(OpCode.JMP_SUB.value).putInt(1 + 4); + // this is return address too + codeByteBuffer.put(OpCode.RET_SUB.value); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertTrue(state.hadFatalError); + } + +} diff --git a/Java/tests/DataOpCodeTests.java b/Java/tests/DataOpCodeTests.java new file mode 100644 index 0000000..6bdf312 --- /dev/null +++ b/Java/tests/DataOpCodeTests.java @@ -0,0 +1,816 @@ +import static common.TestUtils.hexToBytes; +import static org.junit.Assert.*; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.Security; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.ciyam.at.API; +import org.ciyam.at.ExecutionException; +import org.ciyam.at.MachineState; +import org.ciyam.at.OpCode; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import common.TestAPI; +import common.TestLogger; + +public class DataOpCodeTests { + + public TestLogger logger; + public API api; + public MachineState state; + public ByteBuffer codeByteBuffer; + + @BeforeClass + public static void beforeClass() { + Security.insertProviderAt(new BouncyCastleProvider(), 0); + } + + @Before + public void beforeTest() { + logger = new TestLogger(); + api = new TestAPI(); + codeByteBuffer = ByteBuffer.allocate(512).order(ByteOrder.LITTLE_ENDIAN); + } + + @After + public void afterTest() { + codeByteBuffer = null; + api = null; + logger = null; + } + + private void execute() { + System.out.println("Starting execution:"); + System.out.println("Current block height: " + state.currentBlockHeight); + + state.execute(); + + System.out.println("After execution:"); + System.out.println("Steps: " + state.steps); + System.out.println("Program Counter: " + String.format("%04x", state.programCounter)); + System.out.println("Stop Address: " + String.format("%04x", state.onStopAddress)); + System.out.println("Error Address: " + (state.onErrorAddress == null ? "not set" : String.format("%04x", state.onErrorAddress))); + if (state.isSleeping) + System.out.println("Sleeping until current block height (" + state.currentBlockHeight + ") reaches " + state.sleepUntilHeight); + else + System.out.println("Sleeping: " + state.isSleeping); + System.out.println("Stopped: " + state.isStopped); + System.out.println("Finished: " + state.isFinished); + if (state.hadFatalError) + System.out.println("Finished due to fatal error!"); + System.out.println("Frozen: " + state.isFrozen); + } + + private void simulate() { + // version 0003, reserved 0000, code 0200 * 1, data 0020 * 8, call stack 0010 * 4, user stack 0010 * 8 + byte[] headerBytes = hexToBytes("0300" + "0000" + "0002" + "2000" + "1000" + "1000"); + byte[] codeBytes = codeByteBuffer.array(); + byte[] dataBytes = new byte[0]; + + state = new MachineState(api, logger, headerBytes, codeBytes, dataBytes); + + do { + execute(); + + // Bump block height + state.currentBlockHeight++; + } while (!state.isFinished); + + } + + @Test + public void testSET_VAL() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2222L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", 2222L, state.dataByteBuffer.getLong(2 * MachineState.VALUE_SIZE)); + } + + @Test + public void testSET_VALunbounded() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(9999).putLong(2222L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertTrue(state.hadFatalError); + } + + @Test + public void testSET_DAT() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2222L); + codeByteBuffer.put(OpCode.SET_DAT.value).putInt(1).putInt(2); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", 2222L, state.dataByteBuffer.getLong(1 * MachineState.VALUE_SIZE)); + } + + @Test + public void testSET_DATunbounded() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_DAT.value).putInt(9999).putInt(2); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertTrue(state.hadFatalError); + } + + @Test + public void testSET_DATunbounded2() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_DAT.value).putInt(1).putInt(9999); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertTrue(state.hadFatalError); + } + + @Test + public void testCLR_DAT() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2222L); + codeByteBuffer.put(OpCode.CLR_DAT.value).putInt(2); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + + // Check data all zero + state.dataByteBuffer.position(0); + while (state.dataByteBuffer.hasRemaining()) + assertEquals((byte) 0, state.dataByteBuffer.get()); + } + + @Test + public void testCLR_DATunbounded() throws ExecutionException { + codeByteBuffer.put(OpCode.CLR_DAT.value).putInt(9999); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertTrue(state.hadFatalError); + } + + @Test + public void testINC_DAT() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2222L); + codeByteBuffer.put(OpCode.INC_DAT.value).putInt(2); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", 2222L + 1L, state.dataByteBuffer.getLong(2 * MachineState.VALUE_SIZE)); + } + + @Test + public void testINC_DATunbounded() throws ExecutionException { + codeByteBuffer.put(OpCode.INC_DAT.value).putInt(9999); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertTrue(state.hadFatalError); + } + + @Test + public void testINC_DAToverflow() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(0xffffffffffffffffL); + codeByteBuffer.put(OpCode.INC_DAT.value).putInt(2); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", 0L, state.dataByteBuffer.getLong(2 * MachineState.VALUE_SIZE)); + } + + @Test + public void testDEC_DAT() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2222L); + codeByteBuffer.put(OpCode.DEC_DAT.value).putInt(2); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", 2222L - 1L, state.dataByteBuffer.getLong(2 * MachineState.VALUE_SIZE)); + } + + @Test + public void testDEC_DATunbounded() throws ExecutionException { + codeByteBuffer.put(OpCode.DEC_DAT.value).putInt(9999); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + assertTrue(state.isFinished); + assertTrue(state.hadFatalError); + } + + @Test + public void testDEC_DATunderflow() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(0L); + codeByteBuffer.put(OpCode.DEC_DAT.value).putInt(2); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", 0xffffffffffffffffL, state.dataByteBuffer.getLong(2 * MachineState.VALUE_SIZE)); + } + + @Test + public void testADD_DAT() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2222L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(3).putLong(3333L); + codeByteBuffer.put(OpCode.ADD_DAT.value).putInt(2).putInt(3); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", 2222L + 3333L, state.dataByteBuffer.getLong(2 * MachineState.VALUE_SIZE)); + } + + @Test + public void testADD_DATunbounded() throws ExecutionException { + codeByteBuffer.put(OpCode.ADD_DAT.value).putInt(9999).putInt(3); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertTrue(state.hadFatalError); + } + + @Test + public void testADD_DATunbounded2() throws ExecutionException { + codeByteBuffer.put(OpCode.ADD_DAT.value).putInt(2).putInt(9999); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertTrue(state.hadFatalError); + } + + @Test + public void testADD_DAToverflow() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(0x7fffffffffffffffL); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(3).putLong(0x8000000000000099L); + codeByteBuffer.put(OpCode.ADD_DAT.value).putInt(2).putInt(3); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", 0x0000000000000098L, state.dataByteBuffer.getLong(2 * MachineState.VALUE_SIZE)); + } + + @Test + public void testSUB_DAT() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2222L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(3).putLong(3333L); + codeByteBuffer.put(OpCode.SUB_DAT.value).putInt(3).putInt(2); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", 3333L - 2222L, state.dataByteBuffer.getLong(3 * MachineState.VALUE_SIZE)); + } + + @Test + public void testMUL_DAT() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2222L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(3).putLong(3333L); + codeByteBuffer.put(OpCode.MUL_DAT.value).putInt(3).putInt(2); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", (3333L * 2222L), state.dataByteBuffer.getLong(3 * MachineState.VALUE_SIZE)); + } + + @Test + public void testDIV_DAT() throws ExecutionException { + // Note: fatal error because error handler not set + + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2222L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(3).putLong(3333L); + codeByteBuffer.put(OpCode.DIV_DAT.value).putInt(3).putInt(2); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", (3333L / 2222L), state.dataByteBuffer.getLong(3 * MachineState.VALUE_SIZE)); + } + + @Test + public void testDIV_DATzeroWithOnError() throws ExecutionException { + // Note: non-fatal error because error handler IS set + + int errorAddr = 0x29; + + codeByteBuffer.put(OpCode.ERR_ADR.value).putInt(errorAddr); + + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(0L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(3).putLong(3333L); + codeByteBuffer.put(OpCode.DIV_DAT.value).putInt(3).putInt(0); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + // errorAddr: + assertEquals(errorAddr, codeByteBuffer.position()); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Error flag not set", 1L, state.dataByteBuffer.getLong(1 * MachineState.VALUE_SIZE)); + } + + @Test + public void testBOR_DAT() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2222L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(3).putLong(3333L); + codeByteBuffer.put(OpCode.BOR_DAT.value).putInt(3).putInt(2); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", (3333L | 2222L), state.dataByteBuffer.getLong(3 * MachineState.VALUE_SIZE)); + } + + @Test + public void testAND_DAT() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2222L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(3).putLong(3333L); + codeByteBuffer.put(OpCode.AND_DAT.value).putInt(3).putInt(2); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", (3333L & 2222L), state.dataByteBuffer.getLong(3 * MachineState.VALUE_SIZE)); + } + + @Test + public void testXOR_DAT() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2222L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(3).putLong(3333L); + codeByteBuffer.put(OpCode.XOR_DAT.value).putInt(3).putInt(2); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", (3333L ^ 2222L), state.dataByteBuffer.getLong(3 * MachineState.VALUE_SIZE)); + } + + @Test + public void testNOT_DAT() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2222L); + codeByteBuffer.put(OpCode.NOT_DAT.value).putInt(2); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", ~2222L, state.dataByteBuffer.getLong(2 * MachineState.VALUE_SIZE)); + } + + @Test + public void testSET_IND() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(3L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1111L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2222L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(3).putLong(3333L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(4).putLong(4444L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(5).putLong(5555L); + // @(6) = $($0) aka $(3) aka 3333 + codeByteBuffer.put(OpCode.SET_IND.value).putInt(6).putInt(0); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", 3333L, state.dataByteBuffer.getLong(6 * MachineState.VALUE_SIZE)); + } + + @Test + public void testSET_INDunbounded() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(3L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1111L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2222L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(3).putLong(3333L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(4).putLong(4444L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(5).putLong(5555L); + // @(6) = $($9999) but data address 9999 is out of bounds + codeByteBuffer.put(OpCode.SET_IND.value).putInt(6).putInt(9999); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertTrue(state.hadFatalError); + } + + @Test + public void testSET_INDunbounded2() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(9999L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1111L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2222L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(3).putLong(3333L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(4).putLong(4444L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(5).putLong(5555L); + // @(6) = $($0) aka $(9999) but data address 9999 is out of bounds + codeByteBuffer.put(OpCode.SET_IND.value).putInt(6).putInt(0); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertTrue(state.hadFatalError); + } + + @Test + public void testSET_IDX() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1111L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2222L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(3).putLong(3333L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(4).putLong(4444L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(5).putLong(5555L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(6).putLong(1L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(7).putLong(3L); + // @(0) = $($6 + $7) aka $(1 + 3) aka $(4) aka 4444 + codeByteBuffer.put(OpCode.SET_IDX.value).putInt(0).putInt(6).putInt(7); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", 4444L, state.dataByteBuffer.getLong(0 * MachineState.VALUE_SIZE)); + } + + @Test + public void testSET_IDXunbounded() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1111L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2222L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(3).putLong(3333L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(4).putLong(4444L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(5).putLong(5555L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(6).putLong(1L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(7).putLong(3L); + // @(0) = $($9999 + $7) but data address 9999 is out of bounds + codeByteBuffer.put(OpCode.SET_IDX.value).putInt(0).putInt(9999).putInt(7); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertTrue(state.hadFatalError); + } + + @Test + public void testSET_IDXunbounded2() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1111L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2222L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(3).putLong(3333L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(4).putLong(4444L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(5).putLong(5555L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(6).putLong(9999L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(7).putLong(3L); + // @(0) = $($6 + $7) aka $(9999 + 1) but data address 9999 is out of bounds + codeByteBuffer.put(OpCode.SET_IDX.value).putInt(0).putInt(6).putInt(7); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertTrue(state.hadFatalError); + } + + @Test + public void testSET_IDXunbounded3() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1111L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2222L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(3).putLong(3333L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(4).putLong(4444L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(5).putLong(5555L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(6).putLong(1L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(7).putLong(9999L); + // @(0) = $($6 + $7) aka $(1 + 9999) but data address 9999 is out of bounds + codeByteBuffer.put(OpCode.SET_IDX.value).putInt(0).putInt(6).putInt(7); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertTrue(state.hadFatalError); + } + + @Test + public void testSET_IDXunbounded4() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1111L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2222L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(3).putLong(3333L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(4).putLong(4444L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(5).putLong(5555L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(6).putLong(1L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(7).putLong(3L); + // @(0) = $($6 + $9999) but data address 9999 is out of bounds + codeByteBuffer.put(OpCode.SET_IDX.value).putInt(0).putInt(6).putInt(9999); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertTrue(state.hadFatalError); + } + + @Test + public void testIND_DAT() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(3L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1111L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2222L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(3).putLong(3333L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(4).putLong(4444L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(5).putLong(5555L); + // @($0) aka @(3) = $(5) = 5555 + codeByteBuffer.put(OpCode.IND_DAT.value).putInt(0).putInt(5); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", 5555L, state.dataByteBuffer.getLong(3 * MachineState.VALUE_SIZE)); + } + + @Test + public void testIND_DATDunbounded() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(3L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1111L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2222L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(3).putLong(3333L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(4).putLong(4444L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(5).putLong(5555L); + // @($9999) = $(5) but data address 9999 is out of bounds + codeByteBuffer.put(OpCode.SET_IND.value).putInt(9999).putInt(5); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertTrue(state.hadFatalError); + } + + @Test + public void testIND_DATDunbounded2() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(9999L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1111L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2222L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(3).putLong(3333L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(4).putLong(4444L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(5).putLong(5555L); + // @($0) aka @(9999) = $(5) but data address 9999 is out of bounds + codeByteBuffer.put(OpCode.SET_IND.value).putInt(0).putInt(5); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertTrue(state.hadFatalError); + } + + @Test + public void testIDX_DAT() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1111L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2222L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(3).putLong(3333L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(4).putLong(4444L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(5).putLong(5555L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(6).putLong(1L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(7).putLong(3L); + // @($6 + $7) aka @(1 + 3) aka @(4) = $(5) aka 5555 + codeByteBuffer.put(OpCode.IDX_DAT.value).putInt(6).putInt(7).putInt(5); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", 5555L, state.dataByteBuffer.getLong(4 * MachineState.VALUE_SIZE)); + } + + @Test + public void testIDX_DATunbounded() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1111L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2222L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(3).putLong(3333L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(4).putLong(4444L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(5).putLong(5555L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(6).putLong(1L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(7).putLong(3L); + // @($9999 + $7) = $(5) but data address 9999 is out of bounds + codeByteBuffer.put(OpCode.IDX_DAT.value).putInt(9999).putInt(7).putInt(5); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertTrue(state.hadFatalError); + } + + @Test + public void testIDX_DATunbounded2() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1111L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2222L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(3).putLong(3333L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(4).putLong(4444L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(5).putLong(5555L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(6).putLong(9999L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(7).putLong(3L); + // @($6 + $7) aka @(9999 + 3) but data address 9999 is out of bounds + codeByteBuffer.put(OpCode.IDX_DAT.value).putInt(6).putInt(7).putInt(5); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertTrue(state.hadFatalError); + } + + @Test + public void testIDX_DATunbounded3() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1111L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2222L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(3).putLong(3333L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(4).putLong(4444L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(5).putLong(5555L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(6).putLong(1L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(7).putLong(9999L); + // @($6 + $7) aka @(1 + 9999) but data address 9999 is out of bounds + codeByteBuffer.put(OpCode.IDX_DAT.value).putInt(6).putInt(7).putInt(5); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertTrue(state.hadFatalError); + } + + @Test + public void testIDX_DATunbounded4() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1111L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2222L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(3).putLong(3333L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(4).putLong(4444L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(5).putLong(5555L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(6).putLong(1L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(7).putLong(3L); + // @($6 + $9999) = $(5) but data address 9999 is out of bounds + codeByteBuffer.put(OpCode.IDX_DAT.value).putInt(6).putInt(9999).putInt(5); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertTrue(state.hadFatalError); + } + + @Test + public void testMOD_DAT() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2222L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(3).putLong(3333L); + codeByteBuffer.put(OpCode.MOD_DAT.value).putInt(2).putInt(3); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", 2222L % 3333L, state.dataByteBuffer.getLong(2 * MachineState.VALUE_SIZE)); + } + + @Test + public void testMOD_DATzeroWithOnError() throws ExecutionException { + // Note: non-fatal error because error handler IS set + + int errorAddr = 0x29; + + codeByteBuffer.put(OpCode.ERR_ADR.value).putInt(errorAddr); + + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(0L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(3).putLong(3333L); + codeByteBuffer.put(OpCode.MOD_DAT.value).putInt(3).putInt(0); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + // errorAddr: + assertEquals(errorAddr, codeByteBuffer.position()); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Error flag not set", 1L, state.dataByteBuffer.getLong(1 * MachineState.VALUE_SIZE)); + } + + @Test + public void testSHL_DAT() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2222L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(3).putLong(3L); + codeByteBuffer.put(OpCode.SHL_DAT.value).putInt(2).putInt(3); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", 2222L << 3, state.dataByteBuffer.getLong(2 * MachineState.VALUE_SIZE)); + } + + @Test + public void testSHL_DATexcess() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2222L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(3).putLong(3333L); + codeByteBuffer.put(OpCode.SHL_DAT.value).putInt(2).putInt(3); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", 0L, state.dataByteBuffer.getLong(2 * MachineState.VALUE_SIZE)); + } + + @Test + public void testSHR_DAT() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2222L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(3).putLong(3L); + codeByteBuffer.put(OpCode.SHR_DAT.value).putInt(2).putInt(3); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", 2222L >> 3, state.dataByteBuffer.getLong(2 * MachineState.VALUE_SIZE)); + } + + @Test + public void testSHR_DATexcess() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(2222L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(3).putLong(3333L); + codeByteBuffer.put(OpCode.SHR_DAT.value).putInt(2).putInt(3); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", 0L, state.dataByteBuffer.getLong(2 * MachineState.VALUE_SIZE)); + } + +} diff --git a/Java/tests/DisassemblyTests.java b/Java/tests/DisassemblyTests.java new file mode 100644 index 0000000..0812ec2 --- /dev/null +++ b/Java/tests/DisassemblyTests.java @@ -0,0 +1,116 @@ +import static common.TestUtils.hexToBytes; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.Security; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.ciyam.at.API; +import org.ciyam.at.ExecutionException; +import org.ciyam.at.FunctionCode; +import org.ciyam.at.MachineState; +import org.ciyam.at.OpCode; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import common.TestAPI; +import common.TestLogger; + +public class DisassemblyTests { + + public TestLogger logger; + public API api; + public MachineState state; + public ByteBuffer codeByteBuffer; + + @BeforeClass + public static void beforeClass() { + Security.insertProviderAt(new BouncyCastleProvider(), 0); + } + + @Before + public void beforeTest() { + logger = new TestLogger(); + api = new TestAPI(); + codeByteBuffer = ByteBuffer.allocate(512).order(ByteOrder.LITTLE_ENDIAN); + } + + @After + public void afterTest() { + codeByteBuffer = null; + api = null; + logger = null; + } + + private void execute() { + System.out.println("Starting execution:"); + System.out.println("Current block height: " + state.currentBlockHeight); + + state.execute(); + + System.out.println("After execution:"); + System.out.println("Steps: " + state.steps); + System.out.println("Program Counter: " + String.format("%04x", state.programCounter)); + System.out.println("Stop Address: " + String.format("%04x", state.onStopAddress)); + System.out.println("Error Address: " + (state.onErrorAddress == null ? "not set" : String.format("%04x", state.onErrorAddress))); + if (state.isSleeping) + System.out.println("Sleeping until current block height (" + state.currentBlockHeight + ") reaches " + state.sleepUntilHeight); + else + System.out.println("Sleeping: " + state.isSleeping); + System.out.println("Stopped: " + state.isStopped); + System.out.println("Finished: " + state.isFinished); + if (state.hadFatalError) + System.out.println("Finished due to fatal error!"); + System.out.println("Frozen: " + state.isFrozen); + } + + @Test + public void testMD160disassembly() throws ExecutionException { + // MD160 of ffffffffffffffffffffffffffffffffffffffffffffffff is 90e735014ea23aa89190121b229c06d58fc71e83 + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("ffffffffffffffff")); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A1.value).putInt(0); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A2.value).putInt(0); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A3.value).putInt(0); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A4.value).putInt(0); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("90e735014ea23aa8")); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B1.value).putInt(0); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("9190121b229c06d5")); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B2.value).putInt(0); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("8fc71e8300000000")); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B3.value).putInt(0); + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.CHECK_HASH160_A_WITH_B.value).putInt(1); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.ECHO.value).putInt(1); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + // version 0003, reserved 0000, code 0200 * 1, data 0020 * 8, call stack 0010 * 4, user stack 0010 * 4 + byte[] headerBytes = hexToBytes("0300" + "0000" + "0002" + "2000" + "1000" + "1000"); + byte[] codeBytes = codeByteBuffer.array(); + byte[] dataBytes = new byte[0]; + + state = new MachineState(api, logger, headerBytes, codeBytes, dataBytes); + + System.out.println(state.disassemble()); + } + + @Test + public void testACCTdisassembly() throws ExecutionException { + codeByteBuffer.put(hexToBytes("3501030900000006040000000900000029302009000000040000000f1ab4000000330403090000003525010a000000260a00")); + codeByteBuffer.put(hexToBytes("0000320903350703090000003526010a0000001b0a000000cd32280133160100000000331701010000003318010200000033")); + codeByteBuffer.put(hexToBytes("1901030000003505020a0000001b0a000000a1320b033205041e050000001833000509000000320a033203041ab400000033")); + codeByteBuffer.put(hexToBytes("160105000000331701060000003318010700000033190108000000320304320b033203041ab7000000000000000000000000")); + codeByteBuffer.put(hexToBytes("0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")); + codeByteBuffer.put(hexToBytes("000000000000")); + + // version 0003, reserved 0000, code 0200 * 1, data 0020 * 8, call stack 0010 * 4, user stack 0010 * 4 + byte[] headerBytes = hexToBytes("0300" + "0000" + "0002" + "2000" + "1000" + "1000"); + byte[] codeBytes = codeByteBuffer.array(); + byte[] dataBytes = new byte[0]; + + state = new MachineState(api, logger, headerBytes, codeBytes, dataBytes); + + System.out.println(state.disassemble()); + } + +} diff --git a/Java/tests/FunctionCodeTests.java b/Java/tests/FunctionCodeTests.java new file mode 100644 index 0000000..e9fd8df --- /dev/null +++ b/Java/tests/FunctionCodeTests.java @@ -0,0 +1,311 @@ +import static common.TestUtils.hexToBytes; +import static org.junit.Assert.*; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.Security; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.ciyam.at.API; +import org.ciyam.at.ExecutionException; +import org.ciyam.at.FunctionCode; +import org.ciyam.at.MachineState; +import org.ciyam.at.OpCode; +import org.ciyam.at.Timestamp; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import common.TestAPI; +import common.TestLogger; + +public class FunctionCodeTests { + + public TestLogger logger; + public API api; + public MachineState state; + public ByteBuffer codeByteBuffer; + + @BeforeClass + public static void beforeClass() { + Security.insertProviderAt(new BouncyCastleProvider(), 0); + } + + @Before + public void beforeTest() { + logger = new TestLogger(); + api = new TestAPI(); + codeByteBuffer = ByteBuffer.allocate(512).order(ByteOrder.LITTLE_ENDIAN); + } + + @After + public void afterTest() { + codeByteBuffer = null; + api = null; + logger = null; + } + + private void execute() { + System.out.println("Starting execution:"); + System.out.println("Current block height: " + state.currentBlockHeight); + + state.execute(); + + System.out.println("After execution:"); + System.out.println("Steps: " + state.steps); + System.out.println("Program Counter: " + String.format("%04x", state.programCounter)); + System.out.println("Stop Address: " + String.format("%04x", state.onStopAddress)); + System.out.println("Error Address: " + (state.onErrorAddress == null ? "not set" : String.format("%04x", state.onErrorAddress))); + if (state.isSleeping) + System.out.println("Sleeping until current block height (" + state.currentBlockHeight + ") reaches " + state.sleepUntilHeight); + else + System.out.println("Sleeping: " + state.isSleeping); + System.out.println("Stopped: " + state.isStopped); + System.out.println("Finished: " + state.isFinished); + if (state.hadFatalError) + System.out.println("Finished due to fatal error!"); + System.out.println("Frozen: " + state.isFrozen); + } + + private void simulate() { + // version 0003, reserved 0000, code 0200 * 1, data 0020 * 8, call stack 0010 * 4, user stack 0010 * 8 + byte[] headerBytes = hexToBytes("0300" + "0000" + "0002" + "2000" + "1000" + "1000"); + byte[] codeBytes = codeByteBuffer.array(); + byte[] dataBytes = new byte[0]; + + state = new MachineState(api, logger, headerBytes, codeBytes, dataBytes); + + do { + execute(); + + // Bump block height + state.currentBlockHeight++; + } while (!state.isFinished); + + } + + @Test + public void testMD5() throws ExecutionException { + // MD5 of ffffffffffffffffffffffffffffffff is 8d79cbc9a4ecdde112fc91ba625b13c2 + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("ffffffffffffffff")); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A1.value).putInt(0); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A2.value).putInt(0); + // A3 unused + // A4 unused + + codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.MD5_A_TO_B.value); + + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("8d79cbc9a4ecdde1")); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A1.value).putInt(0); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("12fc91ba625b13c2")); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A2.value).putInt(0); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("0000000000000000")); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A3.value).putInt(0); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("0000000000000000")); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A4.value).putInt(0); + + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.CHECK_A_EQUALS_B.value).putInt(1); + + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("MD5 hashes do not match", 1L, state.dataByteBuffer.getLong(1 * MachineState.VALUE_SIZE)); + } + + @Test + public void testCHECK_MD5() throws ExecutionException { + // MD5 of ffffffffffffffffffffffffffffffff is 8d79cbc9a4ecdde112fc91ba625b13c2 + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("ffffffffffffffff")); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A1.value).putInt(0); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A2.value).putInt(0); + // A3 unused + // A4 unused + + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("8d79cbc9a4ecdde1")); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B1.value).putInt(0); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("12fc91ba625b13c2")); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B2.value).putInt(0); + + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.CHECK_MD5_A_WITH_B.value).putInt(1); + + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("MD5 hashes do not match", 1L, state.dataByteBuffer.getLong(1 * MachineState.VALUE_SIZE)); + } + + @Test + public void testHASH160() throws ExecutionException { + // RIPEMD160 of ffffffffffffffffffffffffffffffffffffffffffffffff is 90e735014ea23aa89190121b229c06d58fc71e83 + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("ffffffffffffffff")); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A1.value).putInt(0); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A2.value).putInt(0); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A3.value).putInt(0); + // A4 unused + + codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.HASH160_A_TO_B.value); + + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("90e735014ea23aa8")); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A1.value).putInt(0); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("9190121b229c06d5")); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A2.value).putInt(0); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("8fc71e8300000000")); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A3.value).putInt(0); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("0000000000000000")); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A4.value).putInt(0); + + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.CHECK_A_EQUALS_B.value).putInt(1); + + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("RIPEMD160 hashes do not match", 1L, state.dataByteBuffer.getLong(1 * MachineState.VALUE_SIZE)); + } + + @Test + public void testCHECK_HASH160() throws ExecutionException { + // RIPEMD160 of ffffffffffffffffffffffffffffffffffffffffffffffff is 90e735014ea23aa89190121b229c06d58fc71e83 + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("ffffffffffffffff")); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A1.value).putInt(0); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A2.value).putInt(0); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A3.value).putInt(0); + // A4 unused + + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("90e735014ea23aa8")); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B1.value).putInt(0); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("9190121b229c06d5")); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B2.value).putInt(0); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("8fc71e8300000000")); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B3.value).putInt(0); + + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.CHECK_HASH160_A_WITH_B.value).putInt(1); + + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertEquals("RIPEMD160 hashes do not match", 1L, state.dataByteBuffer.getLong(1 * MachineState.VALUE_SIZE)); + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + } + + @Test + public void testSHA256() throws ExecutionException { + // SHA256 of ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff is af9613760f72635fbdb44a5a0a63c39f12af30f950a6ee5c971be188e89c4051 + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("ffffffffffffffff")); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A1.value).putInt(0); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A2.value).putInt(0); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A3.value).putInt(0); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A4.value).putInt(0); + + codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.SHA256_A_TO_B.value); + + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("af9613760f72635f")); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A1.value).putInt(0); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("bdb44a5a0a63c39f")); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A2.value).putInt(0); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("12af30f950a6ee5c")); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A3.value).putInt(0); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("971be188e89c4051")); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A4.value).putInt(0); + + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.CHECK_A_EQUALS_B.value).putInt(1); + + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("RIPEMD160 hashes do not match", 1L, state.dataByteBuffer.getLong(1 * MachineState.VALUE_SIZE)); + } + + @Test + public void testCHECK_SHA256() throws ExecutionException { + // SHA256 of ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff is af9613760f72635fbdb44a5a0a63c39f12af30f950a6ee5c971be188e89c4051 + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("ffffffffffffffff")); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A1.value).putInt(0); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A2.value).putInt(0); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A3.value).putInt(0); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A4.value).putInt(0); + + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("af9613760f72635f")); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B1.value).putInt(0); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("bdb44a5a0a63c39f")); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B2.value).putInt(0); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("12af30f950a6ee5c")); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B3.value).putInt(0); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("971be188e89c4051")); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B4.value).putInt(0); + + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.CHECK_SHA256_A_WITH_B.value).putInt(1); + + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertEquals("RIPEMD160 hashes do not match", 1L, state.dataByteBuffer.getLong(1 * MachineState.VALUE_SIZE)); + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + } + + @Test + public void testRandom() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(Timestamp.toLong(api.getCurrentBlockHeight(), 0)); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.PUT_TX_AFTER_TIMESTAMP_IN_A.value).putInt(0); + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GENERATE_RANDOM_USING_TX_IN_A.value).putInt(1); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertNotEquals("Random wasn't generated", 0L, state.dataByteBuffer.getLong(1 * MachineState.VALUE_SIZE)); + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + } + + @Test + public void testInvalidFunctionCode() throws ExecutionException { + codeByteBuffer.put(OpCode.EXT_FUN.value).putShort((short) 0xaaaa); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertTrue(state.hadFatalError); + } + + @Test + public void testPlatformSpecific0501() { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(Timestamp.toLong(api.getCurrentBlockHeight(), 0)); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort((short) 0x0501).putInt(0); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + } + + @Test + public void testPlatformSpecific0501Error() { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(Timestamp.toLong(api.getCurrentBlockHeight(), 0)); + codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.value).putShort((short) 0x0501).putInt(0).putInt(0); // Wrong OPCODE for function + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertTrue(state.hadFatalError); + } + +} diff --git a/Java/tests/MiscTests.java b/Java/tests/MiscTests.java new file mode 100644 index 0000000..c0a0d87 --- /dev/null +++ b/Java/tests/MiscTests.java @@ -0,0 +1,111 @@ +import static common.TestUtils.hexToBytes; +import static org.junit.Assert.*; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.Security; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.ciyam.at.API; +import org.ciyam.at.ExecutionException; +import org.ciyam.at.FunctionCode; +import org.ciyam.at.MachineState; +import org.ciyam.at.OpCode; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import common.TestAPI; +import common.TestLogger; + +public class MiscTests { + + public TestLogger logger; + public API api; + public MachineState state; + public ByteBuffer codeByteBuffer; + + @BeforeClass + public static void beforeClass() { + Security.insertProviderAt(new BouncyCastleProvider(), 0); + } + + @Before + public void beforeTest() { + logger = new TestLogger(); + api = new TestAPI(); + codeByteBuffer = ByteBuffer.allocate(512).order(ByteOrder.LITTLE_ENDIAN); + } + + @After + public void afterTest() { + codeByteBuffer = null; + api = null; + logger = null; + } + + private void execute() { + System.out.println("Starting execution:"); + System.out.println("Current block height: " + state.currentBlockHeight); + + state.execute(); + + System.out.println("After execution:"); + System.out.println("Steps: " + state.steps); + System.out.println("Program Counter: " + String.format("%04x", state.programCounter)); + System.out.println("Stop Address: " + String.format("%04x", state.onStopAddress)); + System.out.println("Error Address: " + (state.onErrorAddress == null ? "not set" : String.format("%04x", state.onErrorAddress))); + if (state.isSleeping) + System.out.println("Sleeping until current block height (" + state.currentBlockHeight + ") reaches " + state.sleepUntilHeight); + else + System.out.println("Sleeping: " + state.isSleeping); + System.out.println("Stopped: " + state.isStopped); + System.out.println("Finished: " + state.isFinished); + if (state.hadFatalError) + System.out.println("Finished due to fatal error!"); + System.out.println("Frozen: " + state.isFrozen); + } + + private void simulate() { + // version 0003, reserved 0000, code 0200 * 1, data 0020 * 8, call stack 0010 * 4, user stack 0010 * 8 + byte[] headerBytes = hexToBytes("0300" + "0000" + "0002" + "2000" + "1000" + "1000"); + byte[] codeBytes = codeByteBuffer.array(); + byte[] dataBytes = new byte[0]; + + state = new MachineState(api, logger, headerBytes, codeBytes, dataBytes); + + do { + execute(); + + // Bump block height + state.currentBlockHeight++; + } while (!state.isFinished); + + } + + @Test + public void testSimpleCode() throws ExecutionException { + long testValue = 8888L; + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(testValue); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.ECHO.value).putInt(0); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", testValue, state.dataByteBuffer.getLong(0 * MachineState.VALUE_SIZE)); + } + + @Test + public void testInvalidOpCode() throws ExecutionException { + codeByteBuffer.put((byte) 0xdd); + + simulate(); + + assertTrue(state.isFinished); + assertTrue(state.hadFatalError); + } + +} diff --git a/Java/tests/OpCodeTests.java b/Java/tests/OpCodeTests.java new file mode 100644 index 0000000..7ee530d --- /dev/null +++ b/Java/tests/OpCodeTests.java @@ -0,0 +1,292 @@ +import static common.TestUtils.hexToBytes; +import static org.junit.Assert.*; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.Security; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.ciyam.at.API; +import org.ciyam.at.ExecutionException; +import org.ciyam.at.MachineState; +import org.ciyam.at.OpCode; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import common.TestAPI; +import common.TestLogger; + +public class OpCodeTests { + + public TestLogger logger; + public API api; + public MachineState state; + public ByteBuffer codeByteBuffer; + + @BeforeClass + public static void beforeClass() { + Security.insertProviderAt(new BouncyCastleProvider(), 0); + } + + @Before + public void beforeTest() { + logger = new TestLogger(); + api = new TestAPI(); + codeByteBuffer = ByteBuffer.allocate(512).order(ByteOrder.LITTLE_ENDIAN); + } + + @After + public void afterTest() { + codeByteBuffer = null; + api = null; + logger = null; + } + + private void execute() { + System.out.println("Starting execution:"); + System.out.println("Current block height: " + state.currentBlockHeight); + + state.execute(); + + System.out.println("After execution:"); + System.out.println("Steps: " + state.steps); + System.out.println("Program Counter: " + String.format("%04x", state.programCounter)); + System.out.println("Stop Address: " + String.format("%04x", state.onStopAddress)); + System.out.println("Error Address: " + (state.onErrorAddress == null ? "not set" : String.format("%04x", state.onErrorAddress))); + if (state.isSleeping) + System.out.println("Sleeping until current block height (" + state.currentBlockHeight + ") reaches " + state.sleepUntilHeight); + else + System.out.println("Sleeping: " + state.isSleeping); + System.out.println("Stopped: " + state.isStopped); + System.out.println("Finished: " + state.isFinished); + if (state.hadFatalError) + System.out.println("Finished due to fatal error!"); + System.out.println("Frozen: " + state.isFrozen); + } + + private void simulate() { + // version 0003, reserved 0000, code 0200 * 1, data 0020 * 8, call stack 0010 * 4, user stack 0010 * 8 + byte[] headerBytes = hexToBytes("0300" + "0000" + "0002" + "2000" + "1000" + "1000"); + byte[] codeBytes = codeByteBuffer.array(); + byte[] dataBytes = new byte[0]; + + state = new MachineState(api, logger, headerBytes, codeBytes, dataBytes); + + do { + execute(); + + // Bump block height + state.currentBlockHeight++; + } while (!state.isFinished && !state.isFrozen && !state.isSleeping && !state.isStopped); + + } + + @Test + public void testNOP() throws ExecutionException { + codeByteBuffer.put(OpCode.NOP.value); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + + // Check data unchanged + state.dataByteBuffer.position(0); + while (state.dataByteBuffer.hasRemaining()) + assertEquals((byte) 0, state.dataByteBuffer.get()); + } + + @Test + public void testJMP_ADR() throws ExecutionException { + int targetAddr = 0x12; + + codeByteBuffer.put(OpCode.JMP_ADR.value).putInt(targetAddr); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(1L); + + // targetAddr: + assertEquals(targetAddr, codeByteBuffer.position()); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(2L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Data does not match", 2L, state.dataByteBuffer.getLong(0 * MachineState.VALUE_SIZE)); + } + + @Test + public void testSLP_DAT() throws ExecutionException { + int blockHeight = 12345; + + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(blockHeight); + codeByteBuffer.put(OpCode.SLP_DAT.value).putInt(0); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isSleeping); + assertFalse(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Sleep-until block height incorrect", blockHeight, state.dataByteBuffer.getLong(0 * MachineState.VALUE_SIZE)); + } + + @Test + public void testFIZ_DATtrue() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(0L); + codeByteBuffer.put(OpCode.FIZ_DAT.value).putInt(0); + codeByteBuffer.put(OpCode.SLP_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.isSleeping); + assertFalse(state.hadFatalError); + } + + @Test + public void testFIZ_DATfalse() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(1111L); + codeByteBuffer.put(OpCode.FIZ_DAT.value).putInt(0); + codeByteBuffer.put(OpCode.SLP_IMD.value); + + simulate(); + + assertFalse(state.isFinished); + assertTrue(state.isSleeping); + assertFalse(state.hadFatalError); + } + + @Test + public void testSTZ_DATtrue() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(0L); + codeByteBuffer.put(OpCode.SET_PCS.value); + int stopAddress = codeByteBuffer.position(); + codeByteBuffer.put(OpCode.STZ_DAT.value).putInt(0); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isStopped); + assertFalse(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Program counter incorrect", stopAddress, state.programCounter); + } + + @Test + public void testSTZ_DATfalse() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(1111L); + codeByteBuffer.put(OpCode.SET_PCS.value); + codeByteBuffer.put(OpCode.STZ_DAT.value).putInt(0); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertFalse(state.isStopped); + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + } + + @Test + public void testFIN_IMD() throws ExecutionException { + codeByteBuffer.put(OpCode.FIN_IMD.value); + codeByteBuffer.put(OpCode.STP_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.isStopped); + assertFalse(state.hadFatalError); + } + + @Test + public void testSTP_IMD() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_PCS.value); + int stopAddress = codeByteBuffer.position(); + codeByteBuffer.put(OpCode.NOP.value); + codeByteBuffer.put(OpCode.STP_IMD.value); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isStopped); + assertFalse(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Program counter incorrect", stopAddress, state.programCounter); + } + + @Test + public void testSLP_IMD() throws ExecutionException { + codeByteBuffer.put(OpCode.SLP_IMD.value); + int nextAddress = codeByteBuffer.position(); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isSleeping); + assertFalse(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Program counter incorrect", nextAddress, state.programCounter); + } + + @Test + public void testERR_ADR() throws ExecutionException { + // Note: non-fatal error because error handler IS set + + int errorAddr = 0x29; + + codeByteBuffer.put(OpCode.ERR_ADR.value).putInt(errorAddr); + + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(12345L); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(0L); + codeByteBuffer.put(OpCode.DIV_DAT.value).putInt(0).putInt(1); // divide by zero + codeByteBuffer.put(OpCode.FIN_IMD.value); + + // errorAddr: + assertEquals(errorAddr, codeByteBuffer.position()); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(1L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Error flag not set", 1L, state.dataByteBuffer.getLong(2 * MachineState.VALUE_SIZE)); + } + + @Test + public void testPCS() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("0000000011111111")); + codeByteBuffer.put(OpCode.SET_PCS.value); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("0000000022222222")); + codeByteBuffer.put(OpCode.SET_PCS.value); + codeByteBuffer.put(OpCode.SET_PCS.value); + int expectedStopAddress = codeByteBuffer.position(); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertEquals(expectedStopAddress, state.onStopAddress); + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + } + + @Test + public void testPCS2() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("0000000011111111")); + codeByteBuffer.put(OpCode.SET_PCS.value); + int expectedStopAddress = codeByteBuffer.position(); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("0000000022222222")); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertEquals(expectedStopAddress, state.onStopAddress); + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + } + +} diff --git a/Java/tests/SerializationTests.java b/Java/tests/SerializationTests.java new file mode 100644 index 0000000..bd6e48b --- /dev/null +++ b/Java/tests/SerializationTests.java @@ -0,0 +1,109 @@ +import static common.TestUtils.hexToBytes; +import static org.junit.Assert.*; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; + +import org.ciyam.at.API; +import org.ciyam.at.ExecutionException; +import org.ciyam.at.FunctionCode; +import org.ciyam.at.MachineState; +import org.ciyam.at.OpCode; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import common.TestAPI; +import common.TestLogger; + +public class SerializationTests { + + public TestLogger logger; + public API api; + public MachineState state; + public ByteBuffer codeByteBuffer; + + @Before + public void beforeTest() { + logger = new TestLogger(); + api = new TestAPI(); + codeByteBuffer = ByteBuffer.allocate(256).order(ByteOrder.LITTLE_ENDIAN); + } + + @After + public void afterTest() { + codeByteBuffer = null; + api = null; + logger = null; + } + + private byte[] simulate() { + // version 0003, reserved 0000, code 0100 * 1, data 0020 * 8, call stack 0010 * 4, user stack 0010 * 4 + byte[] headerBytes = hexToBytes("0300" + "0000" + "0001" + "2000" + "1000" + "1000"); + byte[] codeBytes = codeByteBuffer.array(); + byte[] dataBytes = new byte[0]; + + state = new MachineState(api, logger, headerBytes, codeBytes, dataBytes); + + return executeAndCheck(state); + } + + private byte[] continueSimulation(byte[] savedState) { + state = MachineState.fromBytes(api, logger, savedState); + + // Pretend we're on next block + state.currentBlockHeight++; + + return executeAndCheck(state); + } + + private byte[] executeAndCheck(MachineState state) { + state.execute(); + + byte[] stateBytes = state.toBytes(); + MachineState restoredState = MachineState.fromBytes(api, logger, stateBytes); + byte[] restoredStateBytes = restoredState.toBytes(); + + assertTrue("Serialization->Deserialization->Reserialization error", Arrays.equals(stateBytes, restoredStateBytes)); + + return stateBytes; + } + + @Test + public void testPCS2() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("0000000011111111")); + codeByteBuffer.put(OpCode.SET_PCS.value); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).put(hexToBytes("0000000022222222")); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertEquals(0x0e, (int) state.onStopAddress); + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + } + + @Test + public void testStopWithStacks() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(100); // 0000 + codeByteBuffer.put(OpCode.SET_PCS.value); // 000d + codeByteBuffer.put(OpCode.JMP_SUB.value).putInt(0x002a); // 000e + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(10); // 0013 + codeByteBuffer.put(OpCode.ADD_DAT.value).putInt(0).putInt(1); // 0020 + codeByteBuffer.put(OpCode.STP_IMD.value); // 0029 + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.ECHO.value).putInt(0); // 002a + codeByteBuffer.put(OpCode.PSH_DAT.value).putInt(0); // 0031 + codeByteBuffer.put(OpCode.RET_SUB.value); // 0036 + + byte[] savedState = simulate(); + + assertEquals(0x0e, (int) state.onStopAddress); + assertTrue(state.isStopped); + assertFalse(state.hadFatalError); + + savedState = continueSimulation(savedState); + savedState = continueSimulation(savedState); + } + +} diff --git a/Java/tests/TestACCT.java b/Java/tests/TestACCT.java new file mode 100644 index 0000000..37b8d4e --- /dev/null +++ b/Java/tests/TestACCT.java @@ -0,0 +1,218 @@ +import static common.TestUtils.hexToBytes; +import static org.junit.Assert.*; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.Security; +import java.util.Arrays; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.ciyam.at.ExecutionException; +import org.ciyam.at.FunctionCode; +import org.ciyam.at.MachineState; +import org.ciyam.at.OpCode; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import common.ACCTAPI; +import common.TestLogger; + +public class TestACCT { + + public TestLogger logger; + public ACCTAPI api; + public MachineState state; + public ByteBuffer codeByteBuffer; + public ByteBuffer dataByteBuffer; + + @BeforeClass + public static void beforeClass() { + Security.insertProviderAt(new BouncyCastleProvider(), 0); + } + + @Before + public void beforeTest() { + logger = new TestLogger(); + api = new ACCTAPI(); + codeByteBuffer = ByteBuffer.allocate(0x0200 * 1).order(ByteOrder.LITTLE_ENDIAN); + dataByteBuffer = ByteBuffer.allocate(0x0020 * 8).order(ByteOrder.LITTLE_ENDIAN); + } + + @After + public void afterTest() { + dataByteBuffer = null; + codeByteBuffer = null; + api = null; + logger = null; + } + + private byte[] simulate() { + // version 0003, reserved 0000, code 0200 * 1, data 0020 * 8, call stack 0010 * 4, user stack 0010 * 4 + byte[] headerBytes = hexToBytes("0300" + "0000" + "0002" + "2000" + "1000" + "1000"); + byte[] codeBytes = codeByteBuffer.array(); + byte[] dataBytes = dataByteBuffer.array(); + + state = new MachineState(api, logger, headerBytes, codeBytes, dataBytes); + + return executeAndCheck(state); + } + + private byte[] continueSimulation(byte[] savedState) { + state = MachineState.fromBytes(api, logger, savedState); + + // Pretend we're on next block + state.currentBlockHeight++; + + return executeAndCheck(state); + } + + private byte[] executeAndCheck(MachineState state) { + state.execute(); + + byte[] stateBytes = state.toBytes(); + MachineState restoredState = MachineState.fromBytes(api, logger, stateBytes); + byte[] restoredStateBytes = restoredState.toBytes(); + + assertTrue("Serialization->Deserialization->Reserialization error", Arrays.equals(stateBytes, restoredStateBytes)); + + return stateBytes; + } + + @Test + public void testACCT() throws ExecutionException { + // DATA + final int addrHashPart1 = 0x0; + final int addrHashPart2 = 0x1; + final int addrHashPart3 = 0x2; + final int addrHashPart4 = 0x3; + final int addrAddressPart1 = 0x4; + final int addrAddressPart2 = 0x5; + final int addrAddressPart3 = 0x6; + final int addrAddressPart4 = 0x7; + final int addrRefundMinutes = 0x8; + final int addrRefundTimestamp = 0x9; + final int addrLastTimestamp = 0xa; + final int addrBlockTimestamp = 0xb; + final int addrTxType = 0xc; + final int addrComparator = 0xd; + final int addrAddressTemp1 = 0xe; + final int addrAddressTemp2 = 0xf; + final int addrAddressTemp3 = 0x10; + final int addrAddressTemp4 = 0x11; + + byte[] secret = new byte[32]; + new SecureRandom().nextBytes(secret); + + try { + MessageDigest digester = MessageDigest.getInstance("SHA-256"); + byte[] digest = digester.digest(secret); + + dataByteBuffer.put(digest); + } catch (NoSuchAlgorithmException e) { + throw new ExecutionException("No SHA-256 message digest service available", e); + } + + // Destination address (based on "R" for "Responder", where "R" is 0x52) + dataByteBuffer.put(hexToBytes("5200000000000000520000000000000052000000000000005200000000000000")); + + // Expiry in minutes (but actually blocks in this test case) + dataByteBuffer.putLong(8L); + + // Code labels + final int addrTxLoop = 0x36; + final int addrCheckTx = 0x4b; + final int addrCheckSender = 0x64; + final int addrCheckMessage = 0xab; + final int addrPayout = 0xdf; + final int addrRefund = 0x102; + + int tempPC; + + // init: + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_CREATION_TIMESTAMP.value).putInt(addrRefundTimestamp); + codeByteBuffer.put(OpCode.SET_DAT.value).putInt(addrLastTimestamp).putInt(addrRefundTimestamp); + codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.value).putShort(FunctionCode.ADD_MINUTES_TO_TIMESTAMP.value).putInt(addrRefundTimestamp) + .putInt(addrRefundTimestamp).putInt(addrRefundMinutes); + codeByteBuffer.put(OpCode.SET_PCS.value); + + // loop: + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_BLOCK_TIMESTAMP.value).putInt(addrBlockTimestamp); + tempPC = codeByteBuffer.position(); + codeByteBuffer.put(OpCode.BLT_DAT.value).putInt(addrBlockTimestamp).putInt(addrRefundTimestamp).put((byte) (addrTxLoop - tempPC)); + codeByteBuffer.put(OpCode.JMP_ADR.value).putInt(addrRefund); + + // txloop: + assertEquals(addrTxLoop, codeByteBuffer.position()); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.PUT_TX_AFTER_TIMESTAMP_IN_A.value).putInt(addrLastTimestamp); + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.CHECK_A_IS_ZERO.value).putInt(addrComparator); + tempPC = codeByteBuffer.position(); + codeByteBuffer.put(OpCode.BZR_DAT.value).putInt(addrComparator).put((byte) (addrCheckTx - tempPC)); + codeByteBuffer.put(OpCode.STP_IMD.value); + + // checkTx: + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A.value).putInt(addrLastTimestamp); + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_TYPE_FROM_TX_IN_A.value).putInt(addrTxType); + tempPC = codeByteBuffer.position(); + codeByteBuffer.put(OpCode.BNZ_DAT.value).putInt(addrTxType).put((byte) (addrCheckSender - tempPC)); + codeByteBuffer.put(OpCode.JMP_ADR.value).putInt(addrTxLoop); + + // checkSender + assertEquals(addrCheckSender, codeByteBuffer.position()); + codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.PUT_ADDRESS_FROM_TX_IN_A_INTO_B.value); + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_B1.value).putInt(addrAddressTemp1); + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_B2.value).putInt(addrAddressTemp2); + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_B3.value).putInt(addrAddressTemp3); + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_B4.value).putInt(addrAddressTemp4); + tempPC = codeByteBuffer.position(); + codeByteBuffer.put(OpCode.BNE_DAT.value).putInt(addrAddressTemp1).putInt(addrAddressPart1).put((byte) (addrTxLoop - tempPC)); + tempPC = codeByteBuffer.position(); + codeByteBuffer.put(OpCode.BNE_DAT.value).putInt(addrAddressTemp2).putInt(addrAddressPart2).put((byte) (addrTxLoop - tempPC)); + tempPC = codeByteBuffer.position(); + codeByteBuffer.put(OpCode.BNE_DAT.value).putInt(addrAddressTemp3).putInt(addrAddressPart3).put((byte) (addrTxLoop - tempPC)); + tempPC = codeByteBuffer.position(); + codeByteBuffer.put(OpCode.BNE_DAT.value).putInt(addrAddressTemp4).putInt(addrAddressPart4).put((byte) (addrTxLoop - tempPC)); + + // checkMessage: + assertEquals(addrCheckMessage, codeByteBuffer.position()); + codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B.value); + codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.SWAP_A_AND_B.value); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B1.value).putInt(addrHashPart1); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B2.value).putInt(addrHashPart2); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B3.value).putInt(addrHashPart3); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B4.value).putInt(addrHashPart4); + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.CHECK_SHA256_A_WITH_B.value).putInt(addrComparator); + tempPC = codeByteBuffer.position(); + codeByteBuffer.put(OpCode.BNZ_DAT.value).putInt(addrComparator).put((byte) (addrPayout - tempPC)); + codeByteBuffer.put(OpCode.JMP_ADR.value).putInt(addrTxLoop); + + // payout: + assertEquals(addrPayout, codeByteBuffer.position()); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B1.value).putInt(addrAddressPart1); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B2.value).putInt(addrAddressPart2); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B3.value).putInt(addrAddressPart3); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B4.value).putInt(addrAddressPart4); + codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.MESSAGE_A_TO_ADDRESS_IN_B.value); + codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.PAY_ALL_TO_ADDRESS_IN_B.value); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + // refund: + assertEquals(addrRefund, codeByteBuffer.position()); + codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.PUT_CREATOR_INTO_B.value); + codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.PAY_ALL_TO_ADDRESS_IN_B.value); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + byte[] savedState = simulate(); + + while (!state.isFinished) { + ((ACCTAPI) state.api).generateNextBlock(secret); + + savedState = continueSimulation(savedState); + } + } + +} diff --git a/Java/tests/ToolchainTests.java b/Java/tests/ToolchainTests.java new file mode 100644 index 0000000..64ec535 --- /dev/null +++ b/Java/tests/ToolchainTests.java @@ -0,0 +1,18 @@ +import static common.TestUtils.hexToBytes; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; + +import org.junit.Test; + +public class ToolchainTests { + + @Test + public void testHexToBytes() { + assertTrue(Arrays.equals(new byte[] { 0x12 }, hexToBytes("12"))); + assertTrue(Arrays.equals(new byte[] { 0x00, 0x00, 0x12 }, hexToBytes("000012"))); + assertTrue(Arrays.equals(new byte[] { (byte) 0xff }, hexToBytes("ff"))); + assertTrue(Arrays.equals(new byte[] { 0x00, 0x00, (byte) 0xee }, hexToBytes("0000ee"))); + } + +} diff --git a/Java/tests/UserStackOpCodeTests.java b/Java/tests/UserStackOpCodeTests.java new file mode 100644 index 0000000..6c701ed --- /dev/null +++ b/Java/tests/UserStackOpCodeTests.java @@ -0,0 +1,224 @@ +import static common.TestUtils.hexToBytes; +import static org.junit.Assert.*; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.Security; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.ciyam.at.API; +import org.ciyam.at.ExecutionException; +import org.ciyam.at.MachineState; +import org.ciyam.at.OpCode; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import common.TestAPI; +import common.TestLogger; + +public class UserStackOpCodeTests { + + public TestLogger logger; + public API api; + public MachineState state; + public ByteBuffer codeByteBuffer; + + @BeforeClass + public static void beforeClass() { + Security.insertProviderAt(new BouncyCastleProvider(), 0); + } + + @Before + public void beforeTest() { + logger = new TestLogger(); + api = new TestAPI(); + codeByteBuffer = ByteBuffer.allocate(512).order(ByteOrder.LITTLE_ENDIAN); + } + + @After + public void afterTest() { + codeByteBuffer = null; + api = null; + logger = null; + } + + private void execute() { + System.out.println("Starting execution:"); + System.out.println("Current block height: " + state.currentBlockHeight); + + state.execute(); + + System.out.println("After execution:"); + System.out.println("Steps: " + state.steps); + System.out.println("Program Counter: " + String.format("%04x", state.programCounter)); + System.out.println("Stop Address: " + String.format("%04x", state.onStopAddress)); + System.out.println("Error Address: " + (state.onErrorAddress == null ? "not set" : String.format("%04x", state.onErrorAddress))); + if (state.isSleeping) + System.out.println("Sleeping until current block height (" + state.currentBlockHeight + ") reaches " + state.sleepUntilHeight); + else + System.out.println("Sleeping: " + state.isSleeping); + System.out.println("Stopped: " + state.isStopped); + System.out.println("Finished: " + state.isFinished); + if (state.hadFatalError) + System.out.println("Finished due to fatal error!"); + System.out.println("Frozen: " + state.isFrozen); + } + + private void simulate() { + // version 0003, reserved 0000, code 0200 * 1, data 0020 * 8, call stack 0010 * 4, user stack 0010 * 8 + byte[] headerBytes = hexToBytes("0300" + "0000" + "0002" + "2000" + "1000" + "1000"); + byte[] codeBytes = codeByteBuffer.array(); + byte[] dataBytes = new byte[0]; + + state = new MachineState(api, logger, headerBytes, codeBytes, dataBytes); + + do { + execute(); + + // Bump block height + state.currentBlockHeight++; + } while (!state.isFinished); + + } + + @Test + public void testPSH_DAT() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(4444L); + codeByteBuffer.put(OpCode.PSH_DAT.value).putInt(0); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + + int expectedUserStackPosition = (state.numUserStackPages - 1) * MachineState.USER_STACK_PAGE_SIZE; + assertEquals("User stack pointer incorrect", expectedUserStackPosition, state.userStackByteBuffer.position()); + assertEquals("Data does not match", 4444L, state.userStackByteBuffer.getLong(expectedUserStackPosition)); + } + + @Test + public void testPSH_DAT2() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(4444L); + codeByteBuffer.put(OpCode.PSH_DAT.value).putInt(0); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(3333L); + codeByteBuffer.put(OpCode.PSH_DAT.value).putInt(1); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + + int expectedUserStackPosition = (state.numUserStackPages - 2) * MachineState.USER_STACK_PAGE_SIZE; + assertEquals("User stack pointer incorrect", expectedUserStackPosition, state.userStackByteBuffer.position()); + assertEquals("Data does not match", 3333L, state.userStackByteBuffer.getLong(expectedUserStackPosition)); + } + + @Test + public void testPSH_DAToverflow() throws ExecutionException { + // User stack is 0x0010 entries in size, so exceed this to test overflow + for (int i = 0; i < 20; ++i) { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(i).putLong(1000L * i); + codeByteBuffer.put(OpCode.PSH_DAT.value).putInt(i); + } + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertTrue(state.hadFatalError); + } + + @Test + public void testPSH_DAToverflowWithOnError() throws ExecutionException { + int errorAddr = 0x16e; + + codeByteBuffer.put(OpCode.ERR_ADR.value).putInt(errorAddr); + + // User stack is 0x0010 entries in size, so exceed this to test overflow + for (int i = 0; i < 20; ++i) { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(i).putLong(1000L * i); + codeByteBuffer.put(OpCode.PSH_DAT.value).putInt(i); + } + codeByteBuffer.put(OpCode.FIN_IMD.value); + + // errorAddr: + assertEquals(errorAddr, codeByteBuffer.position()); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1L); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + assertEquals("Error flag not set", 1L, state.dataByteBuffer.getLong(1 * MachineState.VALUE_SIZE)); + } + + @Test + public void testPOP_DAT() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(4444L); + codeByteBuffer.put(OpCode.PSH_DAT.value).putInt(0); + codeByteBuffer.put(OpCode.POP_DAT.value).putInt(1); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + + int expectedUserStackPosition = (state.numUserStackPages - 1 + 1) * MachineState.USER_STACK_PAGE_SIZE; + assertEquals("User stack pointer incorrect", expectedUserStackPosition, state.userStackByteBuffer.position()); + assertEquals("Data does not match", 4444L, state.dataByteBuffer.getLong(1 * MachineState.VALUE_SIZE)); + assertEquals("Stack entry not cleared", 0L, state.userStackByteBuffer.getLong(expectedUserStackPosition - MachineState.VALUE_SIZE)); + } + + @Test + public void testPOP_DAT2() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(4444L); + codeByteBuffer.put(OpCode.PSH_DAT.value).putInt(0); + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(3333L); + codeByteBuffer.put(OpCode.PSH_DAT.value).putInt(1); + codeByteBuffer.put(OpCode.POP_DAT.value).putInt(2); + codeByteBuffer.put(OpCode.POP_DAT.value).putInt(3); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertFalse(state.hadFatalError); + + int expectedUserStackPosition = (state.numUserStackPages - 1 - 1 + 1 + 1) * MachineState.USER_STACK_PAGE_SIZE; + assertEquals("User stack pointer incorrect", expectedUserStackPosition, state.userStackByteBuffer.position()); + assertEquals("Data does not match", 3333L, state.dataByteBuffer.getLong(2 * MachineState.VALUE_SIZE)); + assertEquals("Data does not match", 4444L, state.dataByteBuffer.getLong(3 * MachineState.VALUE_SIZE)); + } + + @Test + public void testPOP_DAToverflow() throws ExecutionException { + codeByteBuffer.put(OpCode.POP_DAT.value).putInt(0); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertTrue(state.hadFatalError); + } + + @Test + public void testPOP_DAToverflow2() throws ExecutionException { + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(4444L); + codeByteBuffer.put(OpCode.PSH_DAT.value).putInt(0); + codeByteBuffer.put(OpCode.POP_DAT.value).putInt(1); + codeByteBuffer.put(OpCode.POP_DAT.value).putInt(2); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + simulate(); + + assertTrue(state.isFinished); + assertTrue(state.hadFatalError); + } + +} diff --git a/Java/tests/common/ACCTAPI.java b/Java/tests/common/ACCTAPI.java new file mode 100644 index 0000000..3719c5e --- /dev/null +++ b/Java/tests/common/ACCTAPI.java @@ -0,0 +1,328 @@ +package common; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.stream.Collectors; + +import org.ciyam.at.API; +import org.ciyam.at.ExecutionException; +import org.ciyam.at.FunctionData; +import org.ciyam.at.IllegalFunctionCodeException; +import org.ciyam.at.MachineState; +import org.ciyam.at.Timestamp; + +public class ACCTAPI implements API { + + private class Account { + public String address; + public long balance; + + public Account(String address, long amount) { + this.address = address; + this.balance = amount; + } + } + + private class Transaction { + public int txType; + public String creator; + public String recipient; + public long amount; + public long[] message; + } + + private class Block { + public List transactions; + + public Block() { + this.transactions = new ArrayList(); + } + } + + // + private List blockchain; + private Map accounts; + private long balanceAT; + private long previousBalanceAT; + + // + public ACCTAPI() { + // build blockchain + this.blockchain = new ArrayList(); + + Block genesisBlock = new Block(); + this.blockchain.add(genesisBlock); + + // generate accounts + this.accounts = new HashMap(); + + Account initiator = new Account("Initiator", 0); + this.accounts.put(initiator.address, initiator); + + Account responder = new Account("Responder", 10000); + this.accounts.put(responder.address, responder); + + Account bystander = new Account("Bystander", 999); + this.accounts.put(bystander.address, bystander); + + this.balanceAT = 50000; + this.previousBalanceAT = this.balanceAT; + } + + public void generateNextBlock(byte[] secret) { + Random random = new Random(); + + Block block = new Block(); + + System.out.println("Block " + (this.blockchain.size() + 1)); + + int transactionCount = random.nextInt(5); + + for (int i = 0; i < transactionCount; ++i) { + Transaction transaction = new Transaction(); + + transaction.txType = random.nextInt(2); + + switch (transaction.txType) { + case 0: // payment + transaction.amount = random.nextInt(1000); + System.out.print("Payment Tx [" + transaction.amount + "]"); + break; + + case 1: // message + System.out.print("Message Tx ["); + transaction.message = new long[4]; + + if (random.nextInt(3) == 0) { + // correct message + transaction.message[0] = fromBytes(secret, 0); + transaction.message[1] = fromBytes(secret, 8); + transaction.message[2] = fromBytes(secret, 16); + transaction.message[3] = fromBytes(secret, 24); + } else { + // incorrect message + transaction.message[0] = 0xdeadbeefdeadbeefL; + transaction.message[1] = 0xdeadbeefdeadbeefL; + transaction.message[2] = 0xdeadbeefdeadbeefL; + transaction.message[3] = 0xdeadbeefdeadbeefL; + } + System.out.print(String.format("%016x", transaction.message[0])); + System.out.print(String.format("%016x", transaction.message[1])); + System.out.print(String.format("%016x", transaction.message[2])); + System.out.print(String.format("%016x", transaction.message[3])); + System.out.print("]"); + break; + } + + transaction.creator = getRandomAccount(); + transaction.recipient = getRandomAccount(); + System.out.println(" from " + transaction.creator + " to " + transaction.recipient); + + block.transactions.add(transaction); + } + + this.blockchain.add(block); + + this.previousBalanceAT = this.balanceAT; + } + + /** Convert long to little-endian byte array */ + private byte[] toByteArray(long value) { + return new byte[] { (byte) (value), (byte) (value >> 8), (byte) (value >> 16), (byte) (value >> 24), (byte) (value >> 32), (byte) (value >> 40), + (byte) (value >> 48), (byte) (value >> 56) }; + } + + /** Convert part of little-endian byte[] to long */ + private long fromBytes(byte[] bytes, int start) { + return (bytes[start] & 0xffL) | (bytes[start + 1] & 0xffL) << 8 | (bytes[start + 2] & 0xffL) << 16 | (bytes[start + 3] & 0xffL) << 24 + | (bytes[start + 4] & 0xffL) << 32 | (bytes[start + 5] & 0xffL) << 40 | (bytes[start + 6] & 0xffL) << 48 | (bytes[start + 7] & 0xffL) << 56; + } + + private String getRandomAccount() { + int numAccounts = this.accounts.size(); + int accountIndex = new Random().nextInt(numAccounts); + + List accounts = this.accounts.values().stream().collect(Collectors.toList()); + return accounts.get(accountIndex).address; + } + + @Override + public int getCurrentBlockHeight() { + return this.blockchain.size(); + } + + @Override + public int getATCreationBlockHeight(MachineState state) { + return 1; + } + + @Override + public void putPreviousBlockHashInA(MachineState state) { + state.a1 = this.blockchain.size() - 1; + state.a2 = state.a1; + state.a3 = state.a1; + state.a4 = state.a1; + } + + @Override + public void putTransactionAfterTimestampInA(Timestamp timestamp, MachineState state) { + int blockHeight = timestamp.blockHeight; + int transactionSequence = timestamp.transactionSequence + 1; + + while (blockHeight <= this.blockchain.size()) { + Block block = this.blockchain.get(blockHeight - 1); + + List transactions = block.transactions; + + if (transactionSequence > transactions.size() - 1) { + // No more transactions at this height + ++blockHeight; + transactionSequence = 0; + continue; + } + + Transaction transaction = transactions.get(transactionSequence); + + if (transaction.recipient.equals("Initiator")) { + // Found a transaction + System.out.println("Found transaction at height " + blockHeight + " sequence " + transactionSequence); + + // Generate pseudo-hash of transaction + state.a1 = new Timestamp(blockHeight, transactionSequence).longValue(); + state.a2 = state.a1; + state.a3 = state.a1; + state.a4 = state.a1; + return; + } + + ++transactionSequence; + } + + // Nothing found + state.a1 = 0L; + state.a2 = 0L; + state.a3 = 0L; + state.a4 = 0L; + } + + @Override + public long getTypeFromTransactionInA(MachineState state) { + Timestamp timestamp = new Timestamp(state.a1); + Block block = this.blockchain.get(timestamp.blockHeight - 1); + Transaction transaction = block.transactions.get(timestamp.transactionSequence); + return transaction.txType; + } + + @Override + public long getAmountFromTransactionInA(MachineState state) { + Timestamp timestamp = new Timestamp(state.a1); + Block block = this.blockchain.get(timestamp.blockHeight - 1); + Transaction transaction = block.transactions.get(timestamp.transactionSequence); + return transaction.amount; + } + + @Override + public long getTimestampFromTransactionInA(MachineState state) { + // Transaction hash in A is actually just 4 copies of transaction's "timestamp" + Timestamp timestamp = new Timestamp(state.a1); + return timestamp.longValue(); + } + + @Override + public long generateRandomUsingTransactionInA(MachineState state) { + // NOT USED + return 0L; + } + + @Override + public void putMessageFromTransactionInAIntoB(MachineState state) { + Timestamp timestamp = new Timestamp(state.a1); + Block block = this.blockchain.get(timestamp.blockHeight - 1); + Transaction transaction = block.transactions.get(timestamp.transactionSequence); + state.b1 = transaction.message[0]; + state.b2 = transaction.message[1]; + state.b3 = transaction.message[2]; + state.b4 = transaction.message[3]; + } + + @Override + public void putAddressFromTransactionInAIntoB(MachineState state) { + Timestamp timestamp = new Timestamp(state.a1); + Block block = this.blockchain.get(timestamp.blockHeight - 1); + Transaction transaction = block.transactions.get(timestamp.transactionSequence); + state.b1 = transaction.creator.charAt(0); + state.b2 = state.b1; + state.b3 = state.b1; + state.b4 = state.b1; + } + + @Override + public void putCreatorAddressIntoB(MachineState state) { + // Dummy creator + state.b1 = "C".charAt(0); + state.b2 = state.b1; + state.b3 = state.b1; + state.b4 = state.b1; + } + + @Override + public long getCurrentBalance(MachineState state) { + return this.balanceAT; + } + + @Override + public long getPreviousBalance(MachineState state) { + return this.previousBalanceAT; + } + + @Override + public void payAmountToB(long value1, MachineState state) { + char firstChar = String.format("%c", state.b1).charAt(0); + Account recipient = this.accounts.values().stream().filter((account) -> account.address.charAt(0) == firstChar).findFirst().get(); + recipient.balance += value1; + System.out.println("Paid " + value1 + " to " + recipient.address + ", their balance now: " + recipient.balance); + this.balanceAT -= value1; + System.out.println("Our balance now: " + this.balanceAT); + } + + @Override + public void payCurrentBalanceToB(MachineState state) { + // NOT USED + } + + @Override + public void payPreviousBalanceToB(MachineState state) { + // NOT USED + } + + @Override + public void messageAToB(MachineState state) { + // NOT USED + } + + @Override + public long addMinutesToTimestamp(Timestamp timestamp, long minutes, MachineState state) { + timestamp.blockHeight += (int) minutes; + return timestamp.longValue(); + } + + @Override + public void onFatalError(MachineState state, ExecutionException e) { + System.out.println("Fatal error: " + e.getMessage()); + System.out.println("No error address set - refunding to creator and finishing"); + } + + @Override + public void platformSpecificPreExecuteCheck(short functionCodeValue, int paramCount, boolean returnValueExpected) throws IllegalFunctionCodeException { + // NOT USED + } + + @Override + public void platformSpecificPostCheckExecute(short functionCodeValue, FunctionData functionData, MachineState state) throws ExecutionException { + // NOT USED + } + +} diff --git a/Java/tests/common/TestAPI.java b/Java/tests/common/TestAPI.java new file mode 100644 index 0000000..c2aa819 --- /dev/null +++ b/Java/tests/common/TestAPI.java @@ -0,0 +1,193 @@ +package common; + +import org.ciyam.at.API; +import org.ciyam.at.ExecutionException; +import org.ciyam.at.FunctionData; +import org.ciyam.at.IllegalFunctionCodeException; +import org.ciyam.at.MachineState; +import org.ciyam.at.Timestamp; + +public class TestAPI implements API { + + private static final int BLOCK_PERIOD = 10 * 60; // average period between blocks in seconds + + @Override + public int getCurrentBlockHeight() { + return 10; + } + + @Override + public int getATCreationBlockHeight(MachineState state) { + return 5; + } + + @Override + public void putPreviousBlockHashInA(MachineState state) { + state.a1 = 9L; + state.a2 = 9L; + state.a3 = 9L; + state.a4 = 9L; + } + + @Override + public void putTransactionAfterTimestampInA(Timestamp timestamp, MachineState state) { + // Cycle through transactions: 1 -> 2 -> 3 -> 0 -> 1 ... + state.a1 = (timestamp.transactionSequence + 1) % 4; + state.a2 = state.a1; + state.a3 = state.a1; + state.a4 = state.a1; + } + + @Override + public long getTypeFromTransactionInA(MachineState state) { + return 0L; + } + + @Override + public long getAmountFromTransactionInA(MachineState state) { + return 123L; + } + + @Override + public long getTimestampFromTransactionInA(MachineState state) { + return 1536227162000L; + } + + @Override + public long generateRandomUsingTransactionInA(MachineState state) { + if (state.steps != 0) { + // First call + System.out.println("generateRandomUsingTransactionInA: first call - sleeping"); + + // Perform init? + + state.isSleeping = true; + + return 0L; // not used + } else { + // Second call + System.out.println("generateRandomUsingTransactionInA: second call - returning random"); + + // HASH(A and new block hash) + return (state.a1 ^ 9L) << 3 ^ (state.a2 ^ 9L) << 12 ^ (state.a3 ^ 9L) << 5 ^ (state.a4 ^ 9L); + } + } + + @Override + public void putMessageFromTransactionInAIntoB(MachineState state) { + state.b1 = state.a4; + state.b2 = state.a3; + state.b3 = state.a2; + state.b4 = state.a1; + } + + @Override + public void putAddressFromTransactionInAIntoB(MachineState state) { + // Dummy address + state.b1 = 0xaaaaaaaaaaaaaaaaL; + state.b2 = 0xaaaaaaaaaaaaaaaaL; + state.b3 = 0xaaaaaaaaaaaaaaaaL; + state.b4 = 0xaaaaaaaaaaaaaaaaL; + } + + @Override + public void putCreatorAddressIntoB(MachineState state) { + // Dummy creator + state.b1 = 0xccccccccccccccccL; + state.b2 = 0xccccccccccccccccL; + state.b3 = 0xccccccccccccccccL; + state.b4 = 0xccccccccccccccccL; + } + + @Override + public long getCurrentBalance(MachineState state) { + return 12345L; + } + + @Override + public long getPreviousBalance(MachineState state) { + return 10000L; + } + + @Override + public void payAmountToB(long value1, MachineState state) { + } + + @Override + public void payCurrentBalanceToB(MachineState state) { + } + + @Override + public void payPreviousBalanceToB(MachineState state) { + } + + @Override + public void messageAToB(MachineState state) { + } + + @Override + public long addMinutesToTimestamp(Timestamp timestamp, long minutes, MachineState state) { + timestamp.blockHeight = ((int) minutes * 60) / BLOCK_PERIOD; + return timestamp.longValue(); + } + + @Override + public void onFatalError(MachineState state, ExecutionException e) { + System.out.println("Fatal error: " + e.getMessage()); + System.out.println("No error address set - refunding to creator and finishing"); + } + + @Override + public void platformSpecificPreExecuteCheck(short functionCodeValue, int paramCount, boolean returnValueExpected) throws IllegalFunctionCodeException { + Integer requiredParamCount; + Boolean returnsValue; + + switch (functionCodeValue) { + case 0x0501: + // take one arg, no return value + requiredParamCount = 1; + returnsValue = false; + break; + + case 0x0502: + // take no arg, return a value + requiredParamCount = 0; + returnsValue = true; + break; + + default: + // Unrecognised platform-specific function code + throw new IllegalFunctionCodeException("Unrecognised platform-specific function code 0x" + String.format("%04x", functionCodeValue)); + } + + if (requiredParamCount == null || returnsValue == null) + throw new IllegalFunctionCodeException("Error during platform-specific function pre-execute check"); + + if (paramCount != requiredParamCount) + throw new IllegalFunctionCodeException("Passed paramCount (" + paramCount + ") does not match platform-specific function code 0x" + + String.format("%04x", functionCodeValue) + " required paramCount (" + requiredParamCount + ")"); + + if (returnValueExpected != returnsValue) + throw new IllegalFunctionCodeException("Passed returnValueExpected (" + returnValueExpected + ") does not match platform-specific function code 0x" + + String.format("%04x", functionCodeValue) + " return signature (" + returnsValue + ")"); + } + + @Override + public void platformSpecificPostCheckExecute(short functionCodeValue, FunctionData functionData, MachineState state) throws ExecutionException { + switch (functionCodeValue) { + case 0x0501: + System.out.println("Platform-specific function 0x0501 called with 0x" + String.format("%016x", functionData.value1)); + break; + + case 0x0502: + System.out.println("Platform-specific function 0x0502 called!"); + functionData.returnValue = 0x0502L; + break; + + default: + // Unrecognised platform-specific function code + throw new IllegalFunctionCodeException("Unrecognised platform-specific function code 0x" + String.format("%04x", functionCodeValue)); + } + } + +} diff --git a/Java/tests/common/TestLogger.java b/Java/tests/common/TestLogger.java new file mode 100644 index 0000000..509ca39 --- /dev/null +++ b/Java/tests/common/TestLogger.java @@ -0,0 +1,21 @@ +package common; + +import org.ciyam.at.LoggerInterface; + +public class TestLogger implements LoggerInterface { + @Override + public void error(String message) { + System.err.println("ERROR: " + message); + } + + @Override + public void debug(String message) { + System.err.println("DEBUG: " + message); + } + + @Override + public void echo(String message) { + System.err.println("ECHO: " + message); + } + +} diff --git a/Java/tests/common/TestUtils.java b/Java/tests/common/TestUtils.java new file mode 100644 index 0000000..40a4cea --- /dev/null +++ b/Java/tests/common/TestUtils.java @@ -0,0 +1,21 @@ +package common; + +import java.math.BigInteger; + +public class TestUtils { + + public static byte[] hexToBytes(String hex) { + byte[] output = new byte[hex.length() / 2]; + byte[] converted = new BigInteger("00" + hex, 16).toByteArray(); + + int convertedLength = Math.min(output.length, converted.length); + int convertedStart = converted.length - convertedLength; + + int outputStart = output.length - convertedLength; + + System.arraycopy(converted, convertedStart, output, outputStart, convertedLength); + + return output; + } + +}