From a58b4d4bec2d30466a3069949ba09ad41036ec69 Mon Sep 17 00:00:00 2001 From: catbref Date: Thu, 9 Apr 2020 18:16:14 +0100 Subject: [PATCH] New OpCode.compile() to simplify compilation, with checking! OpCode.compile(...) takes varargs as necessary, and returns byte[]. It will complain if the number of args is incorrect, or if any arg cannot be coerced into the correct form (byte/int/short/long). It will also complain if you try to use a branch offset that is too big to fit into byte. Two-pass compilation support added with OpCode.calcOffset(ByteBuffer, Integer) where Integer is null during the first pass, but also set to ByteBuffer.position() during first pass, so proper value can be calculated during second pass. MachineState.disassemble() changed into a public static method, requiring far less effort to call. Checking of branch target bounds now done by each OpCode as doing it in OpCodeParam wasn't reliable as PC wasn't available. Yet more visibility tightening. Improved comments. Yet more tests. --- .../org/ciyam/at/CompilationException.java | 21 ++ .../main/java/org/ciyam/at/MachineState.java | 7 +- Java/src/main/java/org/ciyam/at/OpCode.java | 57 +++- .../main/java/org/ciyam/at/OpCodeParam.java | 79 ++++- Java/src/main/java/org/ciyam/at/Utils.java | 19 +- .../test/java/org/ciyam/at/CompileTests.java | 287 ++++++++++++++++++ .../java/org/ciyam/at/DisassemblyTests.java | 18 +- .../java/org/ciyam/at/SerializationTests.java | 84 ++--- .../java/org/ciyam/at/test/TestUtils.java | 8 + 9 files changed, 492 insertions(+), 88 deletions(-) create mode 100644 Java/src/main/java/org/ciyam/at/CompilationException.java create mode 100644 Java/src/test/java/org/ciyam/at/CompileTests.java diff --git a/Java/src/main/java/org/ciyam/at/CompilationException.java b/Java/src/main/java/org/ciyam/at/CompilationException.java new file mode 100644 index 0000000..5cfb34f --- /dev/null +++ b/Java/src/main/java/org/ciyam/at/CompilationException.java @@ -0,0 +1,21 @@ +package org.ciyam.at; + +@SuppressWarnings("serial") +public class CompilationException extends ExecutionException { + + public CompilationException() { + } + + public CompilationException(String message) { + super(message); + } + + public CompilationException(Throwable cause) { + super(cause); + } + + public CompilationException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/Java/src/main/java/org/ciyam/at/MachineState.java b/Java/src/main/java/org/ciyam/at/MachineState.java index cc68eb6..d2a04dc 100644 --- a/Java/src/main/java/org/ciyam/at/MachineState.java +++ b/Java/src/main/java/org/ciyam/at/MachineState.java @@ -762,10 +762,11 @@ public class MachineState { } /** Return disassembly of code bytes */ - public String disassemble() throws ExecutionException { + public static String disassemble(byte[] codeBytes, int dataBufferLength) throws ExecutionException { StringBuilder output = new StringBuilder(); - codeByteBuffer.position(0); + ByteBuffer codeByteBuffer = ByteBuffer.wrap(codeBytes); + ByteBuffer dataByteBuffer = ByteBuffer.allocate(dataBufferLength); while (codeByteBuffer.hasRemaining()) { byte rawOpCode = codeByteBuffer.get(); @@ -779,7 +780,7 @@ public class MachineState { if (output.length() != 0) output.append("\n"); - output.append(String.format("[PC: %04x] %s", codeByteBuffer.position() - 1,nextOpCode.disassemble(codeByteBuffer, dataByteBuffer))); + output.append(String.format("[PC: %04x] %s", codeByteBuffer.position() - 1, nextOpCode.disassemble(codeByteBuffer, dataByteBuffer))); } return output.toString(); diff --git a/Java/src/main/java/org/ciyam/at/OpCode.java b/Java/src/main/java/org/ciyam/at/OpCode.java index 574728f..215a1af 100644 --- a/Java/src/main/java/org/ciyam/at/OpCode.java +++ b/Java/src/main/java/org/ciyam/at/OpCode.java @@ -457,10 +457,7 @@ public enum OpCode { int address = (int) args[0]; byte offset = (byte) args[1]; - int branchTarget = state.getProgramCounter() + offset; - - if (branchTarget < 0 || branchTarget >= state.codeByteBuffer.limit()) - throw new InvalidAddressException("branch target out of bounds"); + int branchTarget = calculateBranchTarget(state, offset); long value = state.dataByteBuffer.getLong(address); @@ -480,10 +477,7 @@ public enum OpCode { int address = (int) args[0]; byte offset = (byte) args[1]; - int branchTarget = state.getProgramCounter() + offset; - - if (branchTarget < 0 || branchTarget >= state.codeByteBuffer.limit()) - throw new InvalidAddressException("branch target out of bounds"); + int branchTarget = calculateBranchTarget(state, offset); long value = state.dataByteBuffer.getLong(address); @@ -875,7 +869,7 @@ public enum OpCode { */ protected abstract void executeWithParams(MachineState state, Object... args) throws ExecutionException; - public void execute(MachineState state) throws ExecutionException { + /* package */ void execute(MachineState state) throws ExecutionException { List args = new ArrayList<>(); for (OpCodeParam param : this.params) @@ -884,6 +878,36 @@ public enum OpCode { this.executeWithParams(state, args.toArray()); } + public static int calcOffset(ByteBuffer byteBuffer, Integer branchTarget) { + if (branchTarget == null) + return 0; + + return branchTarget - byteBuffer.position(); + } + + public byte[] compile(Object... args) throws CompilationException { + if (args.length != this.params.length) + throw new IllegalArgumentException(String.format("%s requires %d args, only %d passed", this.name(), this.params.length, args.length)); + + ByteBuffer byteBuffer = ByteBuffer.allocate(32); // 32 should easily be enough + + byteBuffer.put(this.value); + + for (int i = 0; i < this.params.length; ++i) + try { + byteBuffer.put(this.params[i].compile(args[i])); + } catch (ClassCastException e) { + throw new CompilationException(String.format("%s arg[%d] could not coerced to required type", this.name(), i)); + } + + byteBuffer.flip(); + + byte[] bytes = new byte[byteBuffer.limit()]; + byteBuffer.get(bytes); + + return bytes; + } + /** * Returns string representing disassembled OpCode and parameters * @@ -941,10 +965,7 @@ public enum OpCode { int address2 = (int) args[1]; byte offset = (byte) args[2]; - int branchTarget = state.getProgramCounter() + offset; - - if (branchTarget < 0 || branchTarget >= state.codeByteBuffer.limit()) - throw new InvalidAddressException("branch target out of bounds"); + int branchTarget = calculateBranchTarget(state, offset); long value1 = state.dataByteBuffer.getLong(address1); long value2 = state.dataByteBuffer.getLong(address2); @@ -953,4 +974,14 @@ public enum OpCode { state.codeByteBuffer.position(branchTarget); } + protected int calculateBranchTarget(MachineState state, byte offset) throws ExecutionException { + final int branchTarget = state.getProgramCounter() + offset; + + if (branchTarget < 0 || branchTarget >= state.codeByteBuffer.limit()) + throw new InvalidAddressException(String.format("%s code target PC(%04x) + %02x = %04x out of bounds: 0x0000 to 0x%04x", + this.name(), state.getProgramCounter(), offset, branchTarget, state.codeByteBuffer.limit() - 1)); + + return branchTarget; + } + } diff --git a/Java/src/main/java/org/ciyam/at/OpCodeParam.java b/Java/src/main/java/org/ciyam/at/OpCodeParam.java index ea15bf4..b7deb99 100644 --- a/Java/src/main/java/org/ciyam/at/OpCodeParam.java +++ b/Java/src/main/java/org/ciyam/at/OpCodeParam.java @@ -1,10 +1,11 @@ package org.ciyam.at; import java.nio.ByteBuffer; +import java.util.function.Function; enum OpCodeParam { - VALUE { + VALUE(OpCodeParam::compileLong) { @Override public Object fetch(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer) throws ExecutionException { return Long.valueOf(Utils.getCodeValue(codeByteBuffer)); @@ -15,7 +16,7 @@ enum OpCodeParam { return String.format("#%016x", (Long) value); } }, - DEST_ADDR { + DEST_ADDR(OpCodeParam::compileInt) { @Override public Object fetch(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer) throws ExecutionException { return Integer.valueOf(Utils.getDataAddress(codeByteBuffer, dataByteBuffer)); @@ -26,7 +27,7 @@ enum OpCodeParam { return String.format("@%08x", ((Integer) value) / MachineState.VALUE_SIZE); } }, - INDIRECT_DEST_ADDR { + INDIRECT_DEST_ADDR(OpCodeParam::compileInt) { @Override public Object fetch(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer) throws ExecutionException { return Integer.valueOf(Utils.getDataAddress(codeByteBuffer, dataByteBuffer)); @@ -37,7 +38,7 @@ enum OpCodeParam { return String.format("@($%08x)", ((Integer) value) / MachineState.VALUE_SIZE); } }, - INDIRECT_DEST_ADDR_WITH_INDEX { + INDIRECT_DEST_ADDR_WITH_INDEX(OpCodeParam::compileInt) { @Override public Object fetch(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer) throws ExecutionException { return Integer.valueOf(Utils.getDataAddress(codeByteBuffer, dataByteBuffer)); @@ -48,7 +49,7 @@ enum OpCodeParam { return String.format("@($%08x", ((Integer) value) / MachineState.VALUE_SIZE); } }, - SRC_ADDR { + SRC_ADDR(OpCodeParam::compileInt) { @Override public Object fetch(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer) throws ExecutionException { return Integer.valueOf(Utils.getDataAddress(codeByteBuffer, dataByteBuffer)); @@ -59,7 +60,7 @@ enum OpCodeParam { return String.format("$%08x", ((Integer) value) / MachineState.VALUE_SIZE); } }, - INDIRECT_SRC_ADDR { + INDIRECT_SRC_ADDR(OpCodeParam::compileInt) { @Override public Object fetch(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer) throws ExecutionException { return Integer.valueOf(Utils.getDataAddress(codeByteBuffer, dataByteBuffer)); @@ -70,7 +71,7 @@ enum OpCodeParam { return String.format("$($%08x)", ((Integer) value) / MachineState.VALUE_SIZE); } }, - INDIRECT_SRC_ADDR_WITH_INDEX { + INDIRECT_SRC_ADDR_WITH_INDEX(OpCodeParam::compileInt) { @Override public Object fetch(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer) throws ExecutionException { return Integer.valueOf(Utils.getDataAddress(codeByteBuffer, dataByteBuffer)); @@ -81,7 +82,7 @@ enum OpCodeParam { return String.format("$($%08x", ((Integer) value) / MachineState.VALUE_SIZE); } }, - INDEX { + INDEX(OpCodeParam::compileInt) { @Override public Object fetch(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer) throws ExecutionException { return Integer.valueOf(Utils.getDataAddress(codeByteBuffer, dataByteBuffer)); @@ -92,7 +93,7 @@ enum OpCodeParam { return String.format("+ $%08x)", ((Integer) value) / MachineState.VALUE_SIZE); } }, - CODE_ADDR { + CODE_ADDR(OpCodeParam::compileInt) { @Override public Object fetch(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer) throws ExecutionException { return Integer.valueOf(Utils.getCodeAddress(codeByteBuffer)); @@ -103,7 +104,7 @@ enum OpCodeParam { return String.format("[%04x]", (Integer) value); } }, - OFFSET { + OFFSET(OpCodeParam::compileByte) { @Override public Object fetch(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer) throws ExecutionException { return Byte.valueOf(Utils.getCodeOffset(codeByteBuffer)); @@ -114,7 +115,7 @@ enum OpCodeParam { return String.format("PC+%02x=[%04x]", (int) ((Byte) value), postOpcodeProgramCounter - 1 + (Byte) value); } }, - FUNC { + FUNC(OpCodeParam::compileFunc) { @Override public Object fetch(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer) throws ExecutionException { return Short.valueOf(codeByteBuffer.getShort()); @@ -135,7 +136,7 @@ enum OpCodeParam { return "\"" + functionCode.name() + "\"" + String.format("{%04x}", (Short) value); } }, - BLOCK_HEIGHT { + BLOCK_HEIGHT(OpCodeParam::compileInt) { @Override public Object fetch(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer) throws ExecutionException { return Integer.valueOf(codeByteBuffer.getInt()); @@ -147,8 +148,62 @@ enum OpCodeParam { } }; + private final Function compiler; + + private OpCodeParam(Function compiler) { + this.compiler = compiler; + } + public abstract Object fetch(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer) throws ExecutionException; + private static byte[] compileByte(Object o) { + // Highly likely to be an Integer, so try that first + try { + int intValue = (int) o; + if (intValue < Byte.MIN_VALUE || intValue > Byte.MAX_VALUE) + throw new ClassCastException("Value too large to compile to byte"); + + return new byte[] { (byte) intValue }; + } catch (ClassCastException e) { + // Try again using Byte + return new byte[] { (byte) o }; + } + } + + private static byte[] compileShort(Object o) { + short s = (short) o; + return new byte[] { (byte) (s >>> 8), (byte) (s) }; + } + + private static byte[] compileInt(Object o) { + return MachineState.toByteArray((int) o); + } + + private static byte[] compileLong(Object o) { + // Highly likely to be a Long, so try that first + try { + return MachineState.toByteArray((long) o); + } catch (ClassCastException e) { + // Try again using Integer + return MachineState.toByteArray((long)(int) o); + } + } + + private static byte[] compileFunc(Object o) { + try { + FunctionCode func = (FunctionCode) o; + return compileShort(func.value); + } catch (ClassCastException e) { + // Couldn't cast to FunctionCode, + // but try Short in case caller is using API-PASSTHROUGH range + return compileShort(o); + } + } + + protected byte[] compile(Object arg) { + return this.compiler.apply(arg); + } + public String disassemble(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer, int postOpcodeProgramCounter) throws ExecutionException { Object value = fetch(codeByteBuffer, dataByteBuffer); diff --git a/Java/src/main/java/org/ciyam/at/Utils.java b/Java/src/main/java/org/ciyam/at/Utils.java index 4c98281..d03dcb7 100644 --- a/Java/src/main/java/org/ciyam/at/Utils.java +++ b/Java/src/main/java/org/ciyam/at/Utils.java @@ -17,9 +17,9 @@ interface Utils { */ static FunctionCode getFunctionCode(ByteBuffer codeByteBuffer) throws CodeSegmentException, IllegalFunctionCodeException { try { - int rawFunctionCode = codeByteBuffer.getShort(); + final int rawFunctionCode = codeByteBuffer.getShort(); - FunctionCode functionCode = FunctionCode.valueOf(rawFunctionCode); + final FunctionCode functionCode = FunctionCode.valueOf(rawFunctionCode); if (functionCode == null) throw new IllegalFunctionCodeException("Unknown function code"); @@ -44,7 +44,7 @@ interface Utils { */ static int getCodeAddress(ByteBuffer codeByteBuffer) throws CodeSegmentException, InvalidAddressException { try { - int address = codeByteBuffer.getInt(); + final int address = codeByteBuffer.getInt(); if (address < 0 || address > MachineState.MAX_CODE_ADDRESS || address >= codeByteBuffer.limit()) throw new InvalidAddressException("Code address out of bounds"); @@ -69,7 +69,7 @@ interface Utils { */ static int getDataAddress(ByteBuffer codeByteBuffer, ByteBuffer dataByteBuffer) throws CodeSegmentException, InvalidAddressException { try { - int address = codeByteBuffer.getInt() * MachineState.VALUE_SIZE; + final int address = codeByteBuffer.getInt() * MachineState.VALUE_SIZE; if (address < 0 || address + MachineState.VALUE_SIZE >= dataByteBuffer.limit()) throw new InvalidAddressException("Data address out of bounds"); @@ -90,18 +90,11 @@ interface Utils { * @param codeByteBuffer * @return byte offset * @throws CodeSegmentException if we ran out of bytes trying to fetch offset - * @throws InvalidAddressException if position + offset is outside of code segment */ static byte getCodeOffset(ByteBuffer codeByteBuffer) throws CodeSegmentException, InvalidAddressException { try { - final byte offset = codeByteBuffer.get(); - final int target = codeByteBuffer.position() + offset; - - if (target < 0 || target >= codeByteBuffer.limit()) - throw new InvalidAddressException(String.format("Code target PC(%04x) + %02x = %04x out of bounds: 0x0000 to 0x%04x", - codeByteBuffer.position() - 1, offset, target, codeByteBuffer.limit() - 1)); - - return offset; + // We can't do bounds checking here as we don't have access to program counter. + return codeByteBuffer.get(); } catch (BufferUnderflowException e) { throw new CodeSegmentException("No code bytes left to get code offset", e); } diff --git a/Java/src/test/java/org/ciyam/at/CompileTests.java b/Java/src/test/java/org/ciyam/at/CompileTests.java new file mode 100644 index 0000000..4b31f0e --- /dev/null +++ b/Java/src/test/java/org/ciyam/at/CompileTests.java @@ -0,0 +1,287 @@ +package org.ciyam.at; + +import static org.ciyam.at.OpCode.calcOffset; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +import org.ciyam.at.test.TestUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class CompileTests { + + private ByteBuffer codeByteBuffer; + + @Before + public void before() { + this.codeByteBuffer = ByteBuffer.allocate(512); + } + + @After + public void after() { + this.codeByteBuffer = null; + } + + @Test + public void testSimpleCompile() throws CompilationException { + int address = 1234; + long value = System.currentTimeMillis(); + + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(address).putLong(value); + + byte[] expectedBytes = TestUtils.getBytes(codeByteBuffer); + byte[] actualBytes = OpCode.SET_VAL.compile(address, value); + + assertTrue(Arrays.equals(expectedBytes, actualBytes)); + } + + @Test + public void testWideningCompile() throws CompilationException { + int address = 1234; + int value = 9999; + + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(address).putLong(value); + + byte[] expectedBytes = TestUtils.getBytes(codeByteBuffer); + byte[] actualBytes = OpCode.SET_VAL.compile(address, value); + + assertTrue(Arrays.equals(expectedBytes, actualBytes)); + } + + @Test + public void testBranchCompile() throws CompilationException { + int address = 1234; + byte offset = (byte) 16; + + codeByteBuffer.put(OpCode.BZR_DAT.value).putInt(address).put(offset); + + byte[] expectedBytes = TestUtils.getBytes(codeByteBuffer); + byte[] actualBytes = OpCode.BZR_DAT.compile(address, offset); + + assertTrue(Arrays.equals(expectedBytes, actualBytes)); + } + + @Test + public void testBranchNarrowingCompile() throws CompilationException { + int address = 1234; + int offset = 16; // fits within byte + + codeByteBuffer.put(OpCode.BZR_DAT.value).putInt(address).put((byte) offset); + + byte[] expectedBytes = TestUtils.getBytes(codeByteBuffer); + byte[] actualBytes = OpCode.BZR_DAT.compile(address, offset); + + assertTrue(Arrays.equals(expectedBytes, actualBytes)); + } + + @Test + public void testBranchCompileFailure() throws CompilationException { + int address = 1234; + int offset = 9999; // larger than a byte + + try { + codeByteBuffer.put(OpCode.BZR_DAT.value).putInt(address).put((byte) offset); + } catch (Throwable t) { + fail("Narrowing to byte would silently fail with old code"); + } + + try { + byte[] expectedBytes = TestUtils.getBytes(codeByteBuffer); + byte[] actualBytes = OpCode.BZR_DAT.compile(address, offset); + + assertTrue(Arrays.equals(expectedBytes, actualBytes)); + } catch (CompilationException e) { + // this is to be expected as offset is too big to fit into byte + System.out.println("Expected error: " + e.getMessage()); + return; + } + + fail("Narrowing to byte should have caused exception"); + } + + @Test + public void testFunctionCompile() throws CompilationException { + int addrValue1 = 1234; + int addrValue2 = 5678; + int addrResult = 9999; + + codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.value).putShort(FunctionCode.ADD_MINUTES_TO_TIMESTAMP.value).putInt(addrResult).putInt(addrValue1).putInt(addrValue2); + + byte[] expectedBytes = TestUtils.getBytes(codeByteBuffer); + byte[] actualBytes = OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.ADD_MINUTES_TO_TIMESTAMP, addrResult, addrValue1, addrValue2); + + assertTrue(Arrays.equals(expectedBytes, actualBytes)); + } + + @Test + public void testTwoPassCompile() throws CompilationException { + int addrData = 0; + Integer actualTarget = null; + int expectedTarget = 0x06; + + // Old version + codeByteBuffer.put(OpCode.BZR_DAT.value).putInt(addrData).put((byte) expectedTarget); + + // Two-pass version + ByteBuffer compileBuffer = ByteBuffer.allocate(512); + for (int pass = 0; pass < 2; ++pass) { + compileBuffer.clear(); + + compileBuffer.put(OpCode.BZR_DAT.compile(addrData, calcOffset(compileBuffer, actualTarget))); + + actualTarget = compileBuffer.position(); + } + + assertEquals(expectedTarget, (int) actualTarget); + + byte[] expectedBytes = TestUtils.getBytes(codeByteBuffer); + byte[] actualBytes = TestUtils.getBytes(compileBuffer); + + assertTrue(Arrays.equals(expectedBytes, actualBytes)); + } + + @SuppressWarnings("unused") + @Test + public void testComplexCompile() throws CompilationException { + // Labels for data segment addresses + int addrCounter = 0; + // Constants (with corresponding dataByteBuffer.put*() calls below) + final int addrHashPart1 = addrCounter++; + final int addrHashPart2 = addrCounter++; + final int addrHashPart3 = addrCounter++; + final int addrHashPart4 = addrCounter++; + final int addrAddressPart1 = addrCounter++; + final int addrAddressPart2 = addrCounter++; + final int addrAddressPart3 = addrCounter++; + final int addrAddressPart4 = addrCounter++; + final int addrRefundMinutes = addrCounter++; + final int addrHashTempIndex = addrCounter++; + final int addrHashTempLength = addrCounter++; + final int addrInitialPayoutAmount = addrCounter++; + final int addrExpectedTxType = addrCounter++; + // Variables + final int addrRefundTimestamp = addrCounter++; + final int addrLastTimestamp = addrCounter++; + final int addrBlockTimestamp = addrCounter++; + final int addrTxType = addrCounter++; + final int addrComparator = addrCounter++; + final int addrAddressTemp1 = addrCounter++; + final int addrAddressTemp2 = addrCounter++; + final int addrAddressTemp3 = addrCounter++; + final int addrAddressTemp4 = addrCounter++; + final int addrHashTemp1 = addrCounter++; + final int addrHashTemp2 = addrCounter++; + final int addrHashTemp3 = addrCounter++; + final int addrHashTemp4 = addrCounter++; + + Integer labelTxLoop = null; + Integer labelRefund = null; + Integer labelCheckTx = null; + + // Two-pass version + for (int pass = 0; pass < 2; ++pass) { + codeByteBuffer.clear(); + + /* Initialization */ + + // Use AT creation 'timestamp' as starting point for finding transactions sent to AT + codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrLastTimestamp)); + // Calculate refund 'timestamp' by adding minutes to above 'timestamp', then save into addrRefundTimestamp + codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.ADD_MINUTES_TO_TIMESTAMP, addrRefundTimestamp, addrLastTimestamp, addrRefundMinutes)); + + // Load recipient's address into B register + codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrAddressPart1)); + // Send initial payment to recipient so they have enough funds to message AT if all goes well + codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PAY_TO_ADDRESS_IN_B, addrInitialPayoutAmount)); + + // Set restart position to after this opcode + codeByteBuffer.put(OpCode.SET_PCS.compile()); + + /* Main loop */ + + // Fetch current block 'timestamp' + codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_BLOCK_TIMESTAMP, addrBlockTimestamp)); + // If we're not past refund 'timestamp' then look for next transaction + codeByteBuffer.put(OpCode.BLT_DAT.compile(addrBlockTimestamp, addrRefundTimestamp, calcOffset(codeByteBuffer, labelTxLoop))); + // We're past refund 'timestamp' so go refund everything back to AT creator + codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRefund == null ? 0 : labelRefund)); + + /* Transaction processing loop */ + labelTxLoop = codeByteBuffer.position(); + + // Find next transaction to this AT since the last one (if any) + codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTimestamp)); + // If no transaction found, A will be zero. If A is zero, set addrComparator to 1, otherwise 0. + codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.CHECK_A_IS_ZERO, addrComparator)); + // If addrComparator is zero (i.e. A is non-zero, transaction was found) then go check transaction + codeByteBuffer.put(OpCode.BZR_DAT.compile(addrComparator, calcOffset(codeByteBuffer, labelCheckTx))); + // Stop and wait for next block + codeByteBuffer.put(OpCode.STP_IMD.compile()); + + /* Check transaction */ + labelCheckTx = codeByteBuffer.position(); + + // Update our 'last found transaction's timestamp' using 'timestamp' from transaction + codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A, addrLastTimestamp)); + // Extract transaction type (message/payment) from transaction and save type in addrTxType + codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TYPE_FROM_TX_IN_A, addrTxType)); + // If transaction type is not MESSAGE type then go look for another transaction + codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxType, addrExpectedTxType, calcOffset(codeByteBuffer, labelTxLoop))); + + /* Check transaction's sender */ + + // Extract sender address from transaction into B register + codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_ADDRESS_FROM_TX_IN_A_INTO_B)); + // Save B register into data segment starting at addrAddressTemp1 + codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B_IND, addrAddressTemp1)); + // Compare each part of transaction's sender's address with expected address. If they don't match, look for another transaction. + codeByteBuffer.put(OpCode.BNE_DAT.compile(addrAddressTemp1, addrAddressPart1, calcOffset(codeByteBuffer, labelTxLoop))); + codeByteBuffer.put(OpCode.BNE_DAT.compile(addrAddressTemp2, addrAddressPart2, calcOffset(codeByteBuffer, labelTxLoop))); + codeByteBuffer.put(OpCode.BNE_DAT.compile(addrAddressTemp3, addrAddressPart3, calcOffset(codeByteBuffer, labelTxLoop))); + codeByteBuffer.put(OpCode.BNE_DAT.compile(addrAddressTemp4, addrAddressPart4, calcOffset(codeByteBuffer, labelTxLoop))); + + /* Check 'secret' in transaction's message */ + + // Extract message from transaction into B register + codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B)); + // Save B register into data segment starting at addrHashTemp1 + codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B_IND, addrHashTemp1)); + // Load B register with expected hash result + codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrHashPart1)); + // Perform HASH160 using source data at addrHashTemp1 through addrHashTemp4. (Location and length specified via addrHashTempIndex and addrHashTemplength). + // Save the equality result (1 if they match, 0 otherwise) into addrComparator. + codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.CHECK_HASH160_WITH_B, addrComparator, addrHashTempIndex, addrHashTempLength)); + // If hashes don't match, addrComparator will be zero so go find another transaction + codeByteBuffer.put(OpCode.BZR_DAT.compile(addrComparator, calcOffset(codeByteBuffer, labelTxLoop))); + + /* Success! Pay balance to intended recipient */ + + // Load B register with intended recipient address. + codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrAddressPart1)); + // Pay AT's balance to recipient + codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PAY_ALL_TO_ADDRESS_IN_B)); + // We're finished forever + codeByteBuffer.put(OpCode.FIN_IMD.compile()); + + /* Refund balance back to AT creator */ + labelRefund = codeByteBuffer.position(); + + // Load B register with AT creator's address. + codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_CREATOR_INTO_B)); + // Pay AT's balance back to AT's creator. + codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PAY_ALL_TO_ADDRESS_IN_B)); + // We're finished forever + codeByteBuffer.put(OpCode.FIN_IMD.compile()); + } + + byte[] expectedBytes = TestUtils.getBytes(codeByteBuffer); + + assertTrue(expectedBytes.length > 0); + } + +} diff --git a/Java/src/test/java/org/ciyam/at/DisassemblyTests.java b/Java/src/test/java/org/ciyam/at/DisassemblyTests.java index f1b6978..d74a9ca 100644 --- a/Java/src/test/java/org/ciyam/at/DisassemblyTests.java +++ b/Java/src/test/java/org/ciyam/at/DisassemblyTests.java @@ -49,13 +49,10 @@ public class DisassemblyTests extends ExecutableTest { codeByteBuffer.put(OpCode.FIN_IMD.value); - byte[] headerBytes = TestUtils.HEADER_BYTES; - byte[] codeBytes = codeByteBuffer.array(); - byte[] dataBytes = dataByteBuffer.array(); + byte[] codeBytes = TestUtils.getBytes(codeByteBuffer); + int dataBufferLength = dataByteBuffer.position(); - state = new MachineState(api, logger, headerBytes, codeBytes, dataBytes); - - System.out.println(state.disassemble()); + System.out.println(MachineState.disassemble(codeBytes, dataBufferLength)); } @Test @@ -64,16 +61,11 @@ public class DisassemblyTests extends ExecutableTest { byte[] randomCode = new byte[200]; random.nextBytes(randomCode); - codeByteBuffer.put(randomCode); - - byte[] headerBytes = TestUtils.HEADER_BYTES; - byte[] codeBytes = codeByteBuffer.array(); - byte[] dataBytes = dataByteBuffer.array(); - state = new MachineState(api, logger, headerBytes, codeBytes, dataBytes); + int dataBufferLength = 1024; try { - System.out.println(state.disassemble()); + System.out.println(MachineState.disassemble(randomCode, dataBufferLength)); } catch (ExecutionException e) { // we expect this to fail return; diff --git a/Java/src/test/java/org/ciyam/at/SerializationTests.java b/Java/src/test/java/org/ciyam/at/SerializationTests.java index 81d5756..dbe3479 100644 --- a/Java/src/test/java/org/ciyam/at/SerializationTests.java +++ b/Java/src/test/java/org/ciyam/at/SerializationTests.java @@ -15,44 +15,20 @@ import org.junit.Test; public class SerializationTests extends ExecutableTest { - private byte[] simulate() { - byte[] headerBytes = TestUtils.HEADER_BYTES; - 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) { - byte[] codeBytes = codeByteBuffer.array(); - state = MachineState.fromBytes(api, logger, savedState, codeBytes); - - // Pretend we're on next block - api.bumpCurrentBlockHeight(); + @Test + public void testIntToByteArray() { + byte[] expectedBytes = hexToBytes("fedcba98"); + byte[] actualBytes = MachineState.toByteArray(0xfedcba98); - return executeAndCheck(state); + assertTrue(Arrays.equals(expectedBytes, actualBytes)); } - private byte[] executeAndCheck(MachineState state) { - state.execute(); - - // Fetch current state, and code bytes - byte[] stateBytes = unwrapState(state); - byte[] codeBytes = state.getCodeBytes(); - - // Rebuild new MachineState using fetched state & bytes - MachineState restoredState = MachineState.fromBytes(api, logger, stateBytes, codeBytes); - // Extract rebuilt state and code bytes - byte[] restoredStateBytes = restoredState.toBytes(); - byte[] restoredCodeBytes = state.getCodeBytes(); - - // Check that both states and bytes match - assertTrue("Serialization->Deserialization->Reserialization error", Arrays.equals(stateBytes, restoredStateBytes)); - assertTrue("Serialization->Deserialization->Reserialization error", Arrays.equals(codeBytes, restoredCodeBytes)); + @Test + public void testLongToByteArray() { + byte[] expectedBytes = hexToBytes("fedcba9876543210"); + byte[] actualBytes = MachineState.toByteArray(0xfedcba9876543210L); - return stateBytes; + assertTrue(Arrays.equals(expectedBytes, actualBytes)); } /** Test serialization of state with stop address. */ @@ -137,4 +113,44 @@ public class SerializationTests extends ExecutableTest { assertTrue(Arrays.equals(packedState, packedRestoredSate)); } + private byte[] simulate() { + byte[] headerBytes = TestUtils.HEADER_BYTES; + 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) { + byte[] codeBytes = codeByteBuffer.array(); + state = MachineState.fromBytes(api, logger, savedState, codeBytes); + + // Pretend we're on next block + api.bumpCurrentBlockHeight(); + + return executeAndCheck(state); + } + + private byte[] executeAndCheck(MachineState state) { + state.execute(); + + // Fetch current state, and code bytes + byte[] stateBytes = unwrapState(state); + byte[] codeBytes = state.getCodeBytes(); + + // Rebuild new MachineState using fetched state & bytes + MachineState restoredState = MachineState.fromBytes(api, logger, stateBytes, codeBytes); + // Extract rebuilt state and code bytes + byte[] restoredStateBytes = restoredState.toBytes(); + byte[] restoredCodeBytes = state.getCodeBytes(); + + // Check that both states and bytes match + assertTrue("Serialization->Deserialization->Reserialization error", Arrays.equals(stateBytes, restoredStateBytes)); + assertTrue("Serialization->Deserialization->Reserialization error", Arrays.equals(codeBytes, restoredCodeBytes)); + + return stateBytes; + } + } diff --git a/Java/src/test/java/org/ciyam/at/test/TestUtils.java b/Java/src/test/java/org/ciyam/at/test/TestUtils.java index fce6c76..cb093f8 100644 --- a/Java/src/test/java/org/ciyam/at/test/TestUtils.java +++ b/Java/src/test/java/org/ciyam/at/test/TestUtils.java @@ -56,4 +56,12 @@ public class TestUtils { return byteBuffer.array(); } + public static byte[] getBytes(ByteBuffer byteBuffer) { + byteBuffer.flip(); + byte[] bytes = new byte[byteBuffer.limit()]; + byteBuffer.get(bytes); + byteBuffer.clear(); + return bytes; + } + }