From 36c63b0be0a20e3cb668748ec562ed26b8d336a4 Mon Sep 17 00:00:00 2001 From: catbref Date: Mon, 6 Apr 2020 17:37:00 +0100 Subject: [PATCH] Improved testing. Rolled the superior blockchain simulating parts from ACCTAPI into TestAPI. Tried to replace literal test values with named constants from TestAPI class, or derived values. Added some more opcode tests to cover more cases. Renamed some functions of the form "put something ... in A" to "put something .. into A" to help distinguish them from "get ... based on something in A". Added {GET,SET}_[AB]_IND functions as an addition to the long-winded GET_A1..A4, SET_B1..B4. Added more function code tests and separated those tests out into 3 different test classes for manageability. Possible logic error in PAY_TO_ADDRESS_IN_B and PAY_PREVIOUS_TO_ADDRESS_IN_B but needs testing! Improved comments. --- Java/src/main/java/org/ciyam/at/API.java | 8 +- .../main/java/org/ciyam/at/FunctionCode.java | 94 ++++- .../main/java/org/ciyam/at/MachineState.java | 7 +- .../java/BlockchainFunctionCodeTests.java | 219 +++++++++++ Java/src/test/java/DataOpCodeTests.java | 65 +++- Java/src/test/java/FunctionCodeTests.java | 161 ++------ .../test/java/HashingFunctionCodeTests.java | 148 ++++++++ Java/src/test/java/MiscTests.java | 9 +- Java/src/test/java/SerializationTests.java | 70 ++-- Java/src/test/java/common/ACCTAPI.java | 349 ------------------ Java/src/test/java/common/ExecutableTest.java | 11 + Java/src/test/java/common/TestAPI.java | 316 ++++++++++++++-- 12 files changed, 880 insertions(+), 577 deletions(-) create mode 100644 Java/src/test/java/BlockchainFunctionCodeTests.java create mode 100644 Java/src/test/java/HashingFunctionCodeTests.java delete mode 100644 Java/src/test/java/common/ACCTAPI.java diff --git a/Java/src/main/java/org/ciyam/at/API.java b/Java/src/main/java/org/ciyam/at/API.java index 7841d3b..ef34497 100644 --- a/Java/src/main/java/org/ciyam/at/API.java +++ b/Java/src/main/java/org/ciyam/at/API.java @@ -56,11 +56,11 @@ public abstract class API { return getCurrentBlockHeight() - 1; } - /** Put previous block's signature hash in A */ - public abstract void putPreviousBlockHashInA(MachineState state); + /** Put previous block's signature/hash into A */ + public abstract void putPreviousBlockHashIntoA(MachineState state); - /** Put next transaction to AT after timestamp in A, or zero A if no more transactions */ - public abstract void putTransactionAfterTimestampInA(Timestamp timestamp, MachineState state); + /** Put signature/hash of next transaction sent to AT after timestamp in A, or zero A if no more transactions */ + public abstract void putTransactionAfterTimestampIntoA(Timestamp timestamp, MachineState state); /** Return type from transaction in A, or 0xffffffffffffffff if A not valid transaction */ public abstract long getTypeFromTransactionInA(MachineState state); diff --git a/Java/src/main/java/org/ciyam/at/FunctionCode.java b/Java/src/main/java/org/ciyam/at/FunctionCode.java index d03b4e1..ba530ae 100644 --- a/Java/src/main/java/org/ciyam/at/FunctionCode.java +++ b/Java/src/main/java/org/ciyam/at/FunctionCode.java @@ -113,6 +113,44 @@ public enum FunctionCode { functionData.returnValue = state.b4; } }, + /** + * 0x0108
+ * Copies A into addr to addr+3 + */ + GET_A_IND(0x0108, 1, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + // Validate data offset in arg1 + if (functionData.value1 < 0L || functionData.value1 > Integer.MAX_VALUE || functionData.value1 >= state.numDataPages - 3) + throw new ExecutionException(this.name() + " data start address out of bounds"); + + int dataIndex = (int) (functionData.value1 & 0x7fffffffL); + + state.dataByteBuffer.putLong(dataIndex++ * MachineState.VALUE_SIZE, state.a1); + state.dataByteBuffer.putLong(dataIndex++ * MachineState.VALUE_SIZE, state.a2); + state.dataByteBuffer.putLong(dataIndex++ * MachineState.VALUE_SIZE, state.a3); + state.dataByteBuffer.putLong(dataIndex++ * MachineState.VALUE_SIZE, state.a4); + } + }, + /** + * 0x0108
+ * Copies B into addr to addr+3 + */ + GET_B_IND(0x0109, 1, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + // Validate data offset in arg1 + if (functionData.value1 < 0L || functionData.value1 > Integer.MAX_VALUE || functionData.value1 >= state.numDataPages - 3) + throw new ExecutionException(this.name() + " data start address out of bounds"); + + int dataIndex = (int) (functionData.value1 & 0x7fffffffL); + + state.dataByteBuffer.putLong(dataIndex++ * MachineState.VALUE_SIZE, state.b1); + state.dataByteBuffer.putLong(dataIndex++ * MachineState.VALUE_SIZE, state.b2); + state.dataByteBuffer.putLong(dataIndex++ * MachineState.VALUE_SIZE, state.b3); + state.dataByteBuffer.putLong(dataIndex++ * MachineState.VALUE_SIZE, state.b4); + } + }, /** * Set A1
* 0x0110 value @@ -237,6 +275,44 @@ public enum FunctionCode { state.b4 = functionData.value2; } }, + /** + * 0x0108
+ * Copies addr to addr+3 into A + */ + SET_A_IND(0x011c, 1, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + // Validate data offset in arg1 + if (functionData.value1 < 0L || functionData.value1 > Integer.MAX_VALUE || functionData.value1 >= state.numDataPages - 3) + throw new ExecutionException(this.name() + " data start address out of bounds"); + + int dataIndex = (int) (functionData.value1 & 0x7fffffffL); + + state.a1 = state.dataByteBuffer.getLong(dataIndex++ * MachineState.VALUE_SIZE); + state.a2 = state.dataByteBuffer.getLong(dataIndex++ * MachineState.VALUE_SIZE); + state.a3 = state.dataByteBuffer.getLong(dataIndex++ * MachineState.VALUE_SIZE); + state.a4 = state.dataByteBuffer.getLong(dataIndex++ * MachineState.VALUE_SIZE); + } + }, + /** + * 0x0108
+ * Copies addr to addr+3 into B + */ + SET_B_IND(0x011d, 1, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + // Validate data offset in arg1 + if (functionData.value1 < 0L || functionData.value1 > Integer.MAX_VALUE || functionData.value1 >= state.numDataPages - 3) + throw new ExecutionException(this.name() + " data start address out of bounds"); + + int dataIndex = (int) (functionData.value1 & 0x7fffffffL); + + state.b1 = state.dataByteBuffer.getLong(dataIndex++ * MachineState.VALUE_SIZE); + state.b2 = state.dataByteBuffer.getLong(dataIndex++ * MachineState.VALUE_SIZE); + state.b3 = state.dataByteBuffer.getLong(dataIndex++ * MachineState.VALUE_SIZE); + state.b4 = state.dataByteBuffer.getLong(dataIndex++ * MachineState.VALUE_SIZE); + } + }, /** * Clear A
* 0x0120 @@ -720,21 +796,21 @@ public enum FunctionCode { * 0x0303
* Put previous block's hash in A */ - PUT_PREVIOUS_BLOCK_HASH_IN_A(0x0303, 0, false) { + PUT_PREVIOUS_BLOCK_HASH_INTO_A(0x0303, 0, false) { @Override protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { - state.getAPI().putPreviousBlockHashInA(state); + state.getAPI().putPreviousBlockHashIntoA(state); } }, /** * 0x0304
- * Put transaction after timestamp in A, or zero if none
+ * Put transaction (to this AT) 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) { + PUT_TX_AFTER_TIMESTAMP_INTO_A(0x0304, 1, false) { @Override protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { - state.getAPI().putTransactionAfterTimestampInA(new Timestamp(functionData.value1), state); + state.getAPI().putTransactionAfterTimestampIntoA(new Timestamp(functionData.value1), state); } }, /** @@ -854,7 +930,7 @@ public enum FunctionCode { @Override protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { // Reduce amount to current balance if insufficient funds to pay full amount in value1 - long amount = Math.max(state.getCurrentBalance(), functionData.value1); + long amount = Math.min(state.getCurrentBalance(), functionData.value1); // Actually pay state.getAPI().payAmountToB(amount, state); @@ -890,7 +966,7 @@ public enum FunctionCode { @Override protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { // Reduce amount to previous balance if insufficient funds to pay previous balance amount - long amount = Math.max(state.getCurrentBalance(), state.getPreviousBalance()); + long amount = Math.min(state.getCurrentBalance(), state.getPreviousBalance()); // Actually pay state.getAPI().payAmountToB(amount, state); @@ -1003,11 +1079,11 @@ public enum FunctionCode { // TODO: public abstract String disassemble(); protected byte[] getHashData(FunctionData functionData, MachineState state) throws ExecutionException { - // Validate data offset in A1 + // Validate data offset in arg1 if (functionData.value1 < 0L || functionData.value1 > Integer.MAX_VALUE || functionData.value1 >= state.numDataPages) throw new ExecutionException(this.name() + " data start address out of bounds"); - // Validate data length in A2 + // Validate data length in arg2 if (functionData.value2 < 0L || functionData.value2 > Integer.MAX_VALUE || functionData.value1 + byteLengthToDataLength(functionData.value2) > state.numDataPages) throw new ExecutionException(this.name() + " data length invalid"); diff --git a/Java/src/main/java/org/ciyam/at/MachineState.java b/Java/src/main/java/org/ciyam/at/MachineState.java index 9e2995a..a8ce0b6 100644 --- a/Java/src/main/java/org/ciyam/at/MachineState.java +++ b/Java/src/main/java/org/ciyam/at/MachineState.java @@ -28,6 +28,9 @@ public class MachineState { /** Maximum value for an address in the code segment */ public static final int MAX_CODE_ADDRESS = 0x0000ffff; + /** Size of A or B register. */ + public static final int AB_REGISTER_SIZE = 32; + private static class VersionedConstants { /** Bytes per code page */ public final int CODE_PAGE_SIZE; @@ -676,12 +679,12 @@ public class MachineState { } /** Convert int to big-endian byte array */ - private byte[] toByteArray(int value) { + public static byte[] toByteArray(int value) { return new byte[] { (byte) (value >> 24), (byte) (value >> 16), (byte) (value >> 8), (byte) (value) }; } /** Convert long to big-endian byte array */ - private byte[] toByteArray(long value) { + public static byte[] toByteArray(long value) { return new byte[] { (byte) (value >> 56), (byte) (value >> 48), (byte) (value >> 40), (byte) (value >> 32), (byte) (value >> 24), (byte) (value >> 16), (byte) (value >> 8), (byte) (value) }; } diff --git a/Java/src/test/java/BlockchainFunctionCodeTests.java b/Java/src/test/java/BlockchainFunctionCodeTests.java new file mode 100644 index 0000000..fa94e99 --- /dev/null +++ b/Java/src/test/java/BlockchainFunctionCodeTests.java @@ -0,0 +1,219 @@ +import static org.junit.Assert.*; + +import java.util.Arrays; + +import org.ciyam.at.ExecutionException; +import org.ciyam.at.FunctionCode; +import org.ciyam.at.OpCode; +import org.ciyam.at.Timestamp; +import org.junit.Test; + +import common.ExecutableTest; +import common.TestAPI; +import common.TestAPI.TestBlock; +import common.TestAPI.TestTransaction; + +public class BlockchainFunctionCodeTests extends ExecutableTest { + + /** + * GET_BLOCK_TIMESTAMP + * GET_CREATION_TIMESTAMP + * GET_PREVIOUS_BLOCK_TIMESTAMP + * PUT_PREVIOUS_BLOCK_HASH_INTO_A + * PUT_TX_AFTER_TIMESTAMP_INTO_A + * GET_TYPE_FROM_TX_IN_A + * GET_AMOUNT_FROM_TX_IN_A + * GET_TIMESTAMP_FROM_TX_IN_A + * GENERATE_RANDOM_USING_TX_IN_A + * PUT_MESSAGE_FROM_TX_IN_A_INTO_B + * PUT_ADDRESS_FROM_TX_IN_A_INTO_B + * PUT_CREATOR_INTO_B + * GET_CURRENT_BALANCE + * GET_PREVIOUS_BALANCE + * PAY_TO_ADDRESS_IN_B + * PAY_ALL_TO_ADDRESS_IN_B + * PAY_PREVIOUS_TO_ADDRESS_IN_B + * MESSAGE_A_TO_ADDRESS_IN_B + * ADD_MINUTES_TO_TIMESTAMP + */ + + @Test + public void testGetBlockTimestamp() throws ExecutionException { + // Grab block 'timestamp' and save into address 0 + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_BLOCK_TIMESTAMP.value).putInt(0); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + execute(true); + + Timestamp blockTimestamp = new Timestamp(getData(0)); + assertEquals("Block timestamp incorrect", TestAPI.DEFAULT_INITIAL_BLOCK_HEIGHT, blockTimestamp.blockHeight); + + assertTrue(state.getIsFinished()); + assertFalse(state.getHadFatalError()); + } + + @Test + public void testMultipleGetBlockTimestamp() throws ExecutionException { + int expectedBlockHeight = TestAPI.DEFAULT_INITIAL_BLOCK_HEIGHT; + + // Grab block 'timestamp' and save into address 0 + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_BLOCK_TIMESTAMP.value).putInt(0); + codeByteBuffer.put(OpCode.STP_IMD.value); + + execute(true); // TestAPI's block height bumped prior to return + + Timestamp blockTimestamp = new Timestamp(getData(0)); + assertEquals("Block timestamp incorrect", expectedBlockHeight, blockTimestamp.blockHeight); + + // Re-test + ++expectedBlockHeight; + execute(true); // TestAPI's block height bumped prior to return + + blockTimestamp = new Timestamp(getData(0)); + assertEquals("Block timestamp incorrect", expectedBlockHeight, blockTimestamp.blockHeight); + + assertFalse(state.getHadFatalError()); + } + + @Test + public void testGetCreationTimestamp() throws ExecutionException { + // Grab AT creation 'timestamp' and save into address 0 + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_CREATION_TIMESTAMP.value).putInt(0); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + execute(true); + + Timestamp blockTimestamp = new Timestamp(getData(0)); + assertEquals("Block timestamp incorrect", TestAPI.DEFAULT_AT_CREATION_BLOCK_HEIGHT, blockTimestamp.blockHeight); + + assertTrue(state.getIsFinished()); + assertFalse(state.getHadFatalError()); + } + + @Test + public void testGetPreviousBlockTimestamp() throws ExecutionException { + int expectedBlockHeight = TestAPI.DEFAULT_INITIAL_BLOCK_HEIGHT - 1; + + // Grab previous block 'timestamp' and save into address 0 + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_PREVIOUS_BLOCK_TIMESTAMP.value).putInt(0); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + execute(true); + + Timestamp blockTimestamp = new Timestamp(getData(0)); + assertEquals("Block timestamp incorrect", expectedBlockHeight, blockTimestamp.blockHeight); + + assertTrue(state.getIsFinished()); + assertFalse(state.getHadFatalError()); + } + + @Test + public void testPutPreviousBlockHashIntoA() throws ExecutionException { + int previousBlockHeight = TestAPI.DEFAULT_INITIAL_BLOCK_HEIGHT - 1; + + codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.PUT_PREVIOUS_BLOCK_HASH_INTO_A.value); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + execute(true); + + byte[] expectedBlockHash = api.blockchain.get(previousBlockHeight - 1).blockHash; + + byte[] aBytes = state.getA(); + assertTrue("Block hash mismatch", Arrays.equals(expectedBlockHash, aBytes)); + + assertTrue(state.getIsFinished()); + assertFalse(state.getHadFatalError()); + } + + @Test + public void testPutTransactionAfterTimestampIntoA() throws ExecutionException { + long initialTimestamp = Timestamp.toLong(TestAPI.DEFAULT_INITIAL_BLOCK_HEIGHT, 0); + dataByteBuffer.putLong(initialTimestamp); + + // Generate some blocks containing transactions (but none to AT) + api.generateBlockWithNonAtTransactions(); + api.generateBlockWithNonAtTransactions(); + // Generate a block containing transaction to AT + api.generateBlockWithAtTransaction(); + + int currentBlockHeight = api.blockchain.size(); + api.setCurrentBlockHeight(currentBlockHeight); + + // Fetch transaction signature/hash after timestamp stored in address 0 + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A.value).putInt(0); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + execute(true); + + TestTransaction transaction = api.getTransactionFromA(state); + assertNotNull(transaction); + + Timestamp txTimestamp = new Timestamp(transaction.timestamp); + assertEquals("Transaction hash mismatch", currentBlockHeight, txTimestamp.blockHeight); + + assertTrue(state.getIsFinished()); + assertFalse(state.getHadFatalError()); + } + + @Test + public void testPutNoTransactionAfterTimestampIntoA() throws ExecutionException { + int initialBlockHeight = TestAPI.DEFAULT_INITIAL_BLOCK_HEIGHT; + long initialTimestamp = Timestamp.toLong(initialBlockHeight, 0); + dataByteBuffer.putLong(initialTimestamp); + + // Generate a block containing transaction to AT + api.generateBlockWithAtTransaction(); + api.bumpCurrentBlockHeight(); + api.generateBlockWithAtTransaction(); + api.bumpCurrentBlockHeight(); + + long expectedTransactionsCount = 0; + for (int blockHeight = initialBlockHeight + 1; blockHeight <= api.blockchain.size(); ++blockHeight) { + TestBlock block = api.blockchain.get(blockHeight - 1); + expectedTransactionsCount += block.transactions.stream().filter(transaction -> transaction.recipient.equals(TestAPI.AT_ADDRESS)).count(); + } + + // Count how many transactions after timestamp + int targetPosition = 0x15; + + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A.value).putInt(0); + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.CHECK_A_IS_ZERO.value).putInt(1); + int bzrPosition = codeByteBuffer.position(); + codeByteBuffer.put(OpCode.BZR_DAT.value).putInt(1).put((byte) (targetPosition - bzrPosition)); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + assertEquals("targetPosition incorrect", targetPosition, codeByteBuffer.position()); + // Update latest timestamp in address 0 + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A.value).putInt(0); + // Increment transactions count + codeByteBuffer.put(OpCode.INC_DAT.value).putInt(2); + // Loop again + codeByteBuffer.put(OpCode.JMP_ADR.value).putInt(0); + + execute(true); + + long transactionsCount = getData(2); + assertEquals("Transaction count incorrect", expectedTransactionsCount, transactionsCount); + + assertTrue(state.getIsFinished()); + assertFalse(state.getHadFatalError()); + } + + @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_INTO_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); + + // Generate a block containing transaction to AT + api.generateBlockWithAtTransaction(); + + execute(false); + + assertNotEquals("Random wasn't generated", 0L, getData(1)); + assertTrue(state.getIsFinished()); + assertFalse(state.getHadFatalError()); + } + +} diff --git a/Java/src/test/java/DataOpCodeTests.java b/Java/src/test/java/DataOpCodeTests.java index 9382146..5881fbe 100644 --- a/Java/src/test/java/DataOpCodeTests.java +++ b/Java/src/test/java/DataOpCodeTests.java @@ -20,6 +20,7 @@ public class DataOpCodeTests extends ExecutableTest { assertEquals("Data does not match", 2222L, getData(2)); } + /** Check that trying to use an address outside data segment throws a fatal error. */ @Test public void testSET_VALunbounded() throws ExecutionException { codeByteBuffer.put(OpCode.SET_VAL.value).putInt(9999).putLong(2222L); @@ -44,6 +45,7 @@ public class DataOpCodeTests extends ExecutableTest { assertEquals("Data does not match", 2222L, getData(1)); } + /** Check that trying to use an address outside data segment throws a fatal error. */ @Test public void testSET_DATunbounded() throws ExecutionException { codeByteBuffer.put(OpCode.SET_DAT.value).putInt(9999).putInt(2); @@ -55,6 +57,7 @@ public class DataOpCodeTests extends ExecutableTest { assertTrue(state.getHadFatalError()); } + /** Check that trying to use an address outside data segment throws a fatal error. */ @Test public void testSET_DATunbounded2() throws ExecutionException { codeByteBuffer.put(OpCode.SET_DAT.value).putInt(1).putInt(9999); @@ -82,6 +85,7 @@ public class DataOpCodeTests extends ExecutableTest { assertEquals(0L, getData(i)); } + /** Check that trying to use an address outside data segment throws a fatal error. */ @Test public void testCLR_DATunbounded() throws ExecutionException { codeByteBuffer.put(OpCode.CLR_DAT.value).putInt(9999); @@ -106,6 +110,7 @@ public class DataOpCodeTests extends ExecutableTest { assertEquals("Data does not match", 2222L + 1L, getData(2)); } + /** Check that trying to use an address outside data segment throws a fatal error. */ @Test public void testINC_DATunbounded() throws ExecutionException { codeByteBuffer.put(OpCode.INC_DAT.value).putInt(9999); @@ -117,6 +122,7 @@ public class DataOpCodeTests extends ExecutableTest { assertTrue(state.getHadFatalError()); } + /** Check that incrementing maximum unsigned long value overflows back to zero correctly. */ @Test public void testINC_DAToverflow() throws ExecutionException { codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(0xffffffffffffffffL); @@ -143,6 +149,7 @@ public class DataOpCodeTests extends ExecutableTest { assertEquals("Data does not match", 2222L - 1L, getData(2)); } + /** Check that trying to use an address outside data segment throws a fatal error. */ @Test public void testDEC_DATunbounded() throws ExecutionException { codeByteBuffer.put(OpCode.DEC_DAT.value).putInt(9999); @@ -153,6 +160,7 @@ public class DataOpCodeTests extends ExecutableTest { assertTrue(state.getHadFatalError()); } + /** Check that decrementing zero long value underflows back to maximum unsigned long correctly. */ @Test public void testDEC_DATunderflow() throws ExecutionException { codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(0L); @@ -180,6 +188,7 @@ public class DataOpCodeTests extends ExecutableTest { assertEquals("Data does not match", 2222L + 3333L, getData(2)); } + /** Check that trying to use an address outside data segment throws a fatal error. */ @Test public void testADD_DATunbounded() throws ExecutionException { codeByteBuffer.put(OpCode.ADD_DAT.value).putInt(9999).putInt(3); @@ -191,6 +200,7 @@ public class DataOpCodeTests extends ExecutableTest { assertTrue(state.getHadFatalError()); } + /** Check that trying to use an address outside data segment throws a fatal error. */ @Test public void testADD_DATunbounded2() throws ExecutionException { codeByteBuffer.put(OpCode.ADD_DAT.value).putInt(2).putInt(9999); @@ -202,6 +212,7 @@ public class DataOpCodeTests extends ExecutableTest { assertTrue(state.getHadFatalError()); } + /** Check that adding to an unsigned long value overflows correctly. */ @Test public void testADD_DAToverflow() throws ExecutionException { codeByteBuffer.put(OpCode.SET_VAL.value).putInt(2).putLong(0x7fffffffffffffffL); @@ -246,8 +257,6 @@ public class DataOpCodeTests extends ExecutableTest { @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); @@ -260,10 +269,23 @@ public class DataOpCodeTests extends ExecutableTest { assertEquals("Data does not match", (3333L / 2222L), getData(3)); } + /** Check divide-by-zero throws fatal error because error handler not set. */ + @Test + public void testDIV_DATzero() throws ExecutionException { + 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); + + execute(true); + + assertTrue(state.getIsFinished()); + assertTrue(state.getHadFatalError()); + } + + /** Check divide-by-zero is non-fatal because error handler is set. */ @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); @@ -275,6 +297,7 @@ public class DataOpCodeTests extends ExecutableTest { // errorAddr: assertEquals(errorAddr, codeByteBuffer.position()); + // Set 1 at address 1 to indicate we handled error OK codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1L); codeByteBuffer.put(OpCode.FIN_IMD.value); @@ -348,7 +371,9 @@ public class DataOpCodeTests extends ExecutableTest { 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 + // Set address 6 to the value stored in the address pointed to in address 0. + // So, address 0 contains '3', which means use the value stored in address '3', + // and address '3' contains 3333L so save this into address 6. codeByteBuffer.put(OpCode.SET_IND.value).putInt(6).putInt(0); codeByteBuffer.put(OpCode.FIN_IMD.value); @@ -359,6 +384,7 @@ public class DataOpCodeTests extends ExecutableTest { assertEquals("Data does not match", 3333L, getData(6)); } + /** Check that trying to use an address outside data segment throws a fatal error. */ @Test public void testSET_INDunbounded() throws ExecutionException { codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(3L); @@ -377,6 +403,7 @@ public class DataOpCodeTests extends ExecutableTest { assertTrue(state.getHadFatalError()); } + /** Check that trying to use an address outside data segment throws a fatal error. */ @Test public void testSET_INDunbounded2() throws ExecutionException { codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(9999L); @@ -415,6 +442,7 @@ public class DataOpCodeTests extends ExecutableTest { assertEquals("Data does not match", 4444L, getData(0)); } + /** Check that trying to use an address outside data segment throws a fatal error. */ @Test public void testSET_IDXunbounded() throws ExecutionException { codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1111L); @@ -434,6 +462,7 @@ public class DataOpCodeTests extends ExecutableTest { assertTrue(state.getHadFatalError()); } + /** Check that trying to use an address outside data segment throws a fatal error. */ @Test public void testSET_IDXunbounded2() throws ExecutionException { codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1111L); @@ -453,6 +482,7 @@ public class DataOpCodeTests extends ExecutableTest { assertTrue(state.getHadFatalError()); } + /** Check that trying to use an address outside data segment throws a fatal error. */ @Test public void testSET_IDXunbounded3() throws ExecutionException { codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1111L); @@ -472,6 +502,7 @@ public class DataOpCodeTests extends ExecutableTest { assertTrue(state.getHadFatalError()); } + /** Check that trying to use an address outside data segment throws a fatal error. */ @Test public void testSET_IDXunbounded4() throws ExecutionException { codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1111L); @@ -510,6 +541,7 @@ public class DataOpCodeTests extends ExecutableTest { assertEquals("Data does not match", 5555L, getData(3)); } + /** Check that trying to use an address outside data segment throws a fatal error. */ @Test public void testIND_DATDunbounded() throws ExecutionException { codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(3L); @@ -528,6 +560,7 @@ public class DataOpCodeTests extends ExecutableTest { assertTrue(state.getHadFatalError()); } + /** Check that trying to use an address outside data segment throws a fatal error. */ @Test public void testIND_DATDunbounded2() throws ExecutionException { codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(9999L); @@ -566,6 +599,7 @@ public class DataOpCodeTests extends ExecutableTest { assertEquals("Data does not match", 5555L, getData(4)); } + /** Check that trying to use an address outside data segment throws a fatal error. */ @Test public void testIDX_DATunbounded() throws ExecutionException { codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1111L); @@ -585,6 +619,7 @@ public class DataOpCodeTests extends ExecutableTest { assertTrue(state.getHadFatalError()); } + /** Check that trying to use an address outside data segment throws a fatal error. */ @Test public void testIDX_DATunbounded2() throws ExecutionException { codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1111L); @@ -604,6 +639,7 @@ public class DataOpCodeTests extends ExecutableTest { assertTrue(state.getHadFatalError()); } + /** Check that trying to use an address outside data segment throws a fatal error. */ @Test public void testIDX_DATunbounded3() throws ExecutionException { codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1111L); @@ -623,6 +659,7 @@ public class DataOpCodeTests extends ExecutableTest { assertTrue(state.getHadFatalError()); } + /** Check that trying to use an address outside data segment throws a fatal error. */ @Test public void testIDX_DATunbounded4() throws ExecutionException { codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1111L); @@ -656,10 +693,23 @@ public class DataOpCodeTests extends ExecutableTest { assertEquals("Data does not match", 2222L % 3333L, getData(2)); } + /** Check divide-by-zero throws fatal error because error handler not set. */ + @Test + public void testMOD_DATzero() throws ExecutionException { + 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(2).putInt(0); + codeByteBuffer.put(OpCode.FIN_IMD.value); + + execute(true); + + assertTrue(state.getIsFinished()); + assertTrue(state.getHadFatalError()); + } + + /** Check divide-by-zero is non-fatal because error handler is set. */ @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); @@ -671,6 +721,7 @@ public class DataOpCodeTests extends ExecutableTest { // errorAddr: assertEquals(errorAddr, codeByteBuffer.position()); + // Set 1 at address 1 to indicate we handled error OK codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(1L); codeByteBuffer.put(OpCode.FIN_IMD.value); diff --git a/Java/src/test/java/FunctionCodeTests.java b/Java/src/test/java/FunctionCodeTests.java index 2335673..12d979b 100644 --- a/Java/src/test/java/FunctionCodeTests.java +++ b/Java/src/test/java/FunctionCodeTests.java @@ -1,10 +1,10 @@ -import static common.TestUtils.hexToBytes; import static org.junit.Assert.*; -import java.nio.charset.StandardCharsets; +import java.util.Arrays; 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.Test; @@ -13,61 +13,38 @@ import common.ExecutableTest; public class FunctionCodeTests extends ExecutableTest { - private static final String message = "The quick, brown fox jumped over the lazy dog."; - private static final byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8); - - private static final FunctionCode[] bSettingFunctions = new FunctionCode[] { FunctionCode.SET_B1, FunctionCode.SET_B2, FunctionCode.SET_B3, FunctionCode.SET_B4 }; + private static final byte[] TEST_BYTES = "This string is exactly 32 bytes!".getBytes(); @Test - public void testMD5() throws ExecutionException { - testHash("MD5", FunctionCode.MD5_INTO_B, "1388a82384756096e627e3671e2624bf"); - } + public void testABGetSet() throws ExecutionException { + int sourceAddress = 2; + int destAddress = sourceAddress + MachineState.AB_REGISTER_SIZE / MachineState.VALUE_SIZE; - @Test - public void testCHECK_MD5() throws ExecutionException { - checkHash("MD5", FunctionCode.CHECK_MD5_WITH_B, "1388a82384756096e627e3671e2624bf"); - } + // Address of source bytes + dataByteBuffer.putLong(sourceAddress); + // Address where to save bytes + dataByteBuffer.putLong(destAddress); - @Test - public void testRMD160() throws ExecutionException { - testHash("RIPE-MD160", FunctionCode.RMD160_INTO_B, "b5a4b1898af3745dbbb5becb83e72787df9952c9"); - } + // Data to load into A (or B) + assertEquals(sourceAddress * MachineState.VALUE_SIZE, dataByteBuffer.position()); + dataByteBuffer.put(TEST_BYTES); - @Test - public void testCHECK_RMD160() throws ExecutionException { - checkHash("RIPE-MD160", FunctionCode.CHECK_RMD160_WITH_B, "b5a4b1898af3745dbbb5becb83e72787df9952c9"); - } + // Data saved from A (or B) + assertEquals(destAddress * MachineState.VALUE_SIZE, dataByteBuffer.position()); - @Test - public void testSHA256() throws ExecutionException { - testHash("SHA256", FunctionCode.SHA256_INTO_B, "c01d63749ebe5d6b16f7247015cac2e49a5ac4fb6c7f24bed07b8aa904da97f3"); - } - - @Test - public void testCHECK_SHA256() throws ExecutionException { - checkHash("SHA256", FunctionCode.CHECK_SHA256_WITH_B, "c01d63749ebe5d6b16f7247015cac2e49a5ac4fb6c7f24bed07b8aa904da97f3"); - } - - @Test - public void testHASH160() throws ExecutionException { - testHash("HASH160", FunctionCode.HASH160_INTO_B, "54d54a03fd447996ab004dee87fab80bf9477e23"); - } - - @Test - public void testCHECK_HASH160() throws ExecutionException { - checkHash("HASH160", FunctionCode.CHECK_HASH160_WITH_B, "54d54a03fd447996ab004dee87fab80bf9477e23"); - } - - @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); + // Set A register using data pointed to by value held in address 0 + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A_IND.value).putInt(0); + codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.SWAP_A_AND_B.value); + // Save B register to data segment starting at value held in address 1 + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.GET_B_IND.value).putInt(1); codeByteBuffer.put(OpCode.FIN_IMD.value); - execute(false); + execute(true); + + byte[] dest = new byte[TEST_BYTES.length]; + getDataBytes(destAddress, dest); + assertTrue("Data wasn't copied correctly", Arrays.equals(TEST_BYTES, dest)); - assertNotEquals("Random wasn't generated", 0L, getData(1)); assertTrue(state.getIsFinished()); assertFalse(state.getHadFatalError()); } @@ -107,92 +84,4 @@ public class FunctionCodeTests extends ExecutableTest { assertTrue(state.getHadFatalError()); } - private void testHash(String hashName, FunctionCode hashFunction, String expected) throws ExecutionException { - // Data addr 0 for setting values - dataByteBuffer.putLong(0L); - // Data addr 1 for results - dataByteBuffer.putLong(0L); - - // Data addr 2 has start of message bytes (address 4) - dataByteBuffer.putLong(4L); - - // Data addr 3 has length of message bytes - dataByteBuffer.putLong(messageBytes.length); - - // Data addr 4+ for message - dataByteBuffer.put(messageBytes); - - // Actual hash function - codeByteBuffer.put(OpCode.EXT_FUN_DAT_2.value).putShort(hashFunction.value).putInt(2).putInt(3); - - // Hash functions usually put result into B, but we need it in A - codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.SWAP_A_AND_B.value); - - // Expected result goes into B - loadHashIntoB(expected); - - // Check actual hash output (in A) with expected result (in B) and save equality output into address 1 - codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.CHECK_A_EQUALS_B.value).putInt(1); - - codeByteBuffer.put(OpCode.FIN_IMD.value); - - execute(true); - - assertTrue("MachineState isn't in finished state", state.getIsFinished()); - assertFalse("MachineState encountered fatal error", state.getHadFatalError()); - assertEquals(hashName + " hashes do not match", 1L, getData(1)); - } - - private void checkHash(String hashName, FunctionCode checkFunction, String expected) throws ExecutionException { - // Data addr 0 for setting values - dataByteBuffer.putLong(0L); - // Data addr 1 for results - dataByteBuffer.putLong(0L); - - // Data addr 2 has start of message bytes (address 4) - dataByteBuffer.putLong(4L); - - // Data addr 3 has length of message bytes - dataByteBuffer.putLong(messageBytes.length); - - // Data addr 4+ for message - dataByteBuffer.put(messageBytes); - - // Expected result goes into B - loadHashIntoB(expected); - - codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.value).putShort(checkFunction.value).putInt(1).putInt(2).putInt(3); - - codeByteBuffer.put(OpCode.FIN_IMD.value); - - execute(true); - - assertTrue("MachineState isn't in finished state", state.getIsFinished()); - assertFalse("MachineState encountered fatal error", state.getHadFatalError()); - assertEquals(hashName + " hashes do not match", 1L, getData(1)); - } - - private void loadHashIntoB(String expected) { - // Expected result goes into B - codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.CLEAR_B.value); - - // Each 16 hex-chars (8 bytes) fits into each B word (B1, B2, B3 and B4) - int numLongs = (expected.length() + 15) / 16; - - for (int longIndex = 0; longIndex < numLongs; ++longIndex) { - final int endIndex = expected.length() - (numLongs - longIndex - 1) * 16; - final int beginIndex = Math.max(0, endIndex - 16); - - String hexChars = expected.substring(beginIndex, endIndex); - - codeByteBuffer.put(OpCode.SET_VAL.value); - codeByteBuffer.putInt(0); // addr 0 - codeByteBuffer.put(new byte[8 - hexChars.length() / 2]); // pad LSB with zeros - codeByteBuffer.put(hexToBytes(hexChars)); - - final FunctionCode bSettingFunction = bSettingFunctions[longIndex]; - codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(bSettingFunction.value).putInt(0); - } - } - } diff --git a/Java/src/test/java/HashingFunctionCodeTests.java b/Java/src/test/java/HashingFunctionCodeTests.java new file mode 100644 index 0000000..ca5feb9 --- /dev/null +++ b/Java/src/test/java/HashingFunctionCodeTests.java @@ -0,0 +1,148 @@ +import static common.TestUtils.hexToBytes; +import static org.junit.Assert.*; + +import java.nio.charset.StandardCharsets; + +import org.ciyam.at.ExecutionException; +import org.ciyam.at.FunctionCode; +import org.ciyam.at.OpCode; +import org.junit.Test; + +import common.ExecutableTest; + +public class HashingFunctionCodeTests extends ExecutableTest { + + private static final String message = "The quick, brown fox jumped over the lazy dog."; + private static final byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8); + + private static final FunctionCode[] bSettingFunctions = new FunctionCode[] { FunctionCode.SET_B1, FunctionCode.SET_B2, FunctionCode.SET_B3, FunctionCode.SET_B4 }; + + @Test + public void testMD5() throws ExecutionException { + testHash("MD5", FunctionCode.MD5_INTO_B, "1388a82384756096e627e3671e2624bf"); + } + + @Test + public void testCHECK_MD5() throws ExecutionException { + checkHash("MD5", FunctionCode.CHECK_MD5_WITH_B, "1388a82384756096e627e3671e2624bf"); + } + + @Test + public void testRMD160() throws ExecutionException { + testHash("RIPE-MD160", FunctionCode.RMD160_INTO_B, "b5a4b1898af3745dbbb5becb83e72787df9952c9"); + } + + @Test + public void testCHECK_RMD160() throws ExecutionException { + checkHash("RIPE-MD160", FunctionCode.CHECK_RMD160_WITH_B, "b5a4b1898af3745dbbb5becb83e72787df9952c9"); + } + + @Test + public void testSHA256() throws ExecutionException { + testHash("SHA256", FunctionCode.SHA256_INTO_B, "c01d63749ebe5d6b16f7247015cac2e49a5ac4fb6c7f24bed07b8aa904da97f3"); + } + + @Test + public void testCHECK_SHA256() throws ExecutionException { + checkHash("SHA256", FunctionCode.CHECK_SHA256_WITH_B, "c01d63749ebe5d6b16f7247015cac2e49a5ac4fb6c7f24bed07b8aa904da97f3"); + } + + @Test + public void testHASH160() throws ExecutionException { + testHash("HASH160", FunctionCode.HASH160_INTO_B, "54d54a03fd447996ab004dee87fab80bf9477e23"); + } + + @Test + public void testCHECK_HASH160() throws ExecutionException { + checkHash("HASH160", FunctionCode.CHECK_HASH160_WITH_B, "54d54a03fd447996ab004dee87fab80bf9477e23"); + } + + private void testHash(String hashName, FunctionCode hashFunction, String expected) throws ExecutionException { + // Data addr 0 for setting values + dataByteBuffer.putLong(0L); + // Data addr 1 for results + dataByteBuffer.putLong(0L); + + // Data addr 2 has start of message bytes (address 4) + dataByteBuffer.putLong(4L); + + // Data addr 3 has length of message bytes + dataByteBuffer.putLong(messageBytes.length); + + // Data addr 4+ for message + dataByteBuffer.put(messageBytes); + + // Actual hash function + codeByteBuffer.put(OpCode.EXT_FUN_DAT_2.value).putShort(hashFunction.value).putInt(2).putInt(3); + + // Hash functions usually put result into B, but we need it in A + codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.SWAP_A_AND_B.value); + + // Expected result goes into B + loadHashIntoB(expected); + + // Check actual hash output (in A) with expected result (in B) and save equality output into address 1 + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.CHECK_A_EQUALS_B.value).putInt(1); + + codeByteBuffer.put(OpCode.FIN_IMD.value); + + execute(true); + + assertTrue("MachineState isn't in finished state", state.getIsFinished()); + assertFalse("MachineState encountered fatal error", state.getHadFatalError()); + assertEquals(hashName + " hashes do not match", 1L, getData(1)); + } + + private void checkHash(String hashName, FunctionCode checkFunction, String expected) throws ExecutionException { + // Data addr 0 for setting values + dataByteBuffer.putLong(0L); + // Data addr 1 for results + dataByteBuffer.putLong(0L); + + // Data addr 2 has start of message bytes (address 4) + dataByteBuffer.putLong(4L); + + // Data addr 3 has length of message bytes + dataByteBuffer.putLong(messageBytes.length); + + // Data addr 4+ for message + dataByteBuffer.put(messageBytes); + + // Expected result goes into B + loadHashIntoB(expected); + + codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.value).putShort(checkFunction.value).putInt(1).putInt(2).putInt(3); + + codeByteBuffer.put(OpCode.FIN_IMD.value); + + execute(true); + + assertTrue("MachineState isn't in finished state", state.getIsFinished()); + assertFalse("MachineState encountered fatal error", state.getHadFatalError()); + assertEquals(hashName + " hashes do not match", 1L, getData(1)); + } + + private void loadHashIntoB(String expected) { + // Expected result goes into B + codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.CLEAR_B.value); + + // Each 16 hex-chars (8 bytes) fits into each B word (B1, B2, B3 and B4) + int numLongs = (expected.length() + 15) / 16; + + for (int longIndex = 0; longIndex < numLongs; ++longIndex) { + final int endIndex = expected.length() - (numLongs - longIndex - 1) * 16; + final int beginIndex = Math.max(0, endIndex - 16); + + String hexChars = expected.substring(beginIndex, endIndex); + + codeByteBuffer.put(OpCode.SET_VAL.value); + codeByteBuffer.putInt(0); // addr 0 + codeByteBuffer.put(new byte[8 - hexChars.length() / 2]); // pad LSB with zeros + codeByteBuffer.put(hexToBytes(hexChars)); + + final FunctionCode bSettingFunction = bSettingFunctions[longIndex]; + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(bSettingFunction.value).putInt(0); + } + } + +} diff --git a/Java/src/test/java/MiscTests.java b/Java/src/test/java/MiscTests.java index 8e7a625..d2fed94 100644 --- a/Java/src/test/java/MiscTests.java +++ b/Java/src/test/java/MiscTests.java @@ -7,6 +7,7 @@ import org.ciyam.at.OpCode; import org.junit.Test; import common.ExecutableTest; +import common.TestAPI; import common.TestUtils; public class MiscTests extends ExecutableTest { @@ -40,8 +41,9 @@ public class MiscTests extends ExecutableTest { // Infinite loop codeByteBuffer.put(OpCode.JMP_ADR.value).putInt(0); - // If starting balance is 1234 then should take about 3 rounds as 500 steps max each round. - for (int i = 0; i < 3; ++i) + // We need enough rounds to exhaust balance + long minRounds = TestAPI.DEFAULT_INITIAL_BALANCE / TestAPI.MAX_STEPS_PER_ROUND + 1; + for (long i = 0; i < minRounds; ++i) execute(true); assertTrue(state.getIsFrozen()); @@ -52,7 +54,8 @@ public class MiscTests extends ExecutableTest { @Test public void testMinActivation() throws ExecutionException { - long minActivationAmount = 12345L; // 0x0000000000003039 + // Make sure minimum activation amount is greater than initial balance + long minActivationAmount = TestAPI.DEFAULT_INITIAL_BALANCE * 2L; byte[] headerBytes = TestUtils.toHeaderBytes(TestUtils.VERSION, TestUtils.NUM_CODE_PAGES, TestUtils.NUM_DATA_PAGES, TestUtils.NUM_CALL_STACK_PAGES, TestUtils.NUM_USER_STACK_PAGES, minActivationAmount); byte[] codeBytes = codeByteBuffer.array(); diff --git a/Java/src/test/java/SerializationTests.java b/Java/src/test/java/SerializationTests.java index 52bec63..4edb668 100644 --- a/Java/src/test/java/SerializationTests.java +++ b/Java/src/test/java/SerializationTests.java @@ -1,41 +1,18 @@ import static common.TestUtils.hexToBytes; import static org.junit.Assert.*; -import java.nio.ByteBuffer; import java.util.Arrays; 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; +import common.ExecutableTest; import common.TestUtils; -public class SerializationTests { - - public TestLogger logger; - public TestAPI api; - public MachineState state; - public ByteBuffer codeByteBuffer; - - @Before - public void beforeTest() { - logger = new TestLogger(); - api = new TestAPI(); - codeByteBuffer = ByteBuffer.allocate(512); - } - - @After - public void afterTest() { - codeByteBuffer = null; - api = null; - logger = null; - } +public class SerializationTests extends ExecutableTest { private byte[] simulate() { byte[] headerBytes = TestUtils.HEADER_BYTES; @@ -60,52 +37,85 @@ public class SerializationTests { private byte[] executeAndCheck(MachineState state) { state.execute(); - byte[] stateBytes = state.toBytes(); + // 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; } + /** Test serialization of state with stop address. */ @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(0x0e, (int) state.getOnStopAddress()); + assertEquals(expectedStopAddress, (int) state.getOnStopAddress()); assertTrue(state.getIsFinished()); assertFalse(state.getHadFatalError()); } + /** Test serialization of state with data pushed onto user stack. */ @Test public void testStopWithStacks() throws ExecutionException { - codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(100); // 0000 + long initialValue = 100L; + long increment = 10L; + + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(0).putLong(initialValue); // 0000 + codeByteBuffer.put(OpCode.SET_PCS.value); // 000d + int expectedStopAddress = codeByteBuffer.position(); + codeByteBuffer.put(OpCode.JMP_SUB.value).putInt(0x002a); // 000e - codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(10); // 0013 + + codeByteBuffer.put(OpCode.SET_VAL.value).putInt(1).putLong(increment); // 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.getOnStopAddress()); + assertEquals(expectedStopAddress, (int) state.getOnStopAddress()); assertTrue(state.getIsStopped()); assertFalse(state.getHadFatalError()); + // Just after first STP_IMD we expect address 0 to be initialValue + increment + long expectedValue = initialValue + increment; + assertEquals(expectedValue, getData(0)); + + // Perform another execution round savedState = continueSimulation(savedState); + expectedValue += increment; + assertEquals(expectedValue, getData(0)); + savedState = continueSimulation(savedState); + expectedValue += increment; + assertEquals(expectedValue, getData(0)); } } diff --git a/Java/src/test/java/common/ACCTAPI.java b/Java/src/test/java/common/ACCTAPI.java deleted file mode 100644 index 7c553cb..0000000 --- a/Java/src/test/java/common/ACCTAPI.java +++ /dev/null @@ -1,349 +0,0 @@ -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.OpCode; -import org.ciyam.at.Timestamp; - -public class ACCTAPI extends 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; - - // - 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); - - Account creator = new Account("Creator", 0); - this.accounts.put(creator.address, creator); - - this.balanceAT = 50000; - } - - 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); - } - - /** Convert long to big-endian byte array */ - @SuppressWarnings("unused") - private byte[] toByteArray(long value) { - return new byte[] { (byte) (value >> 56), (byte) (value >> 48), (byte) (value >> 40), (byte) (value >> 32), - (byte) (value >> 24), (byte) (value >> 16), (byte) (value >> 8), (byte) (value) }; - } - - /** Convert part of big-endian byte[] to long */ - private long fromBytes(byte[] bytes, int start) { - return (bytes[start] & 0xffL) << 56 | (bytes[start + 1] & 0xffL) << 48 | (bytes[start + 2] & 0xffL) << 40 | (bytes[start + 3] & 0xffL) << 32 - | (bytes[start + 4] & 0xffL) << 24 | (bytes[start + 5] & 0xffL) << 16 | (bytes[start + 6] & 0xffL) << 8 | (bytes[start + 7] & 0xffL); - } - - 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 getMaxStepsPerRound() { - return 500; - } - - @Override - public int getOpCodeSteps(OpCode opcode) { - return 1; - } - - @Override - public long getFeePerStep() { - return 1L; - } - - @Override - public int getCurrentBlockHeight() { - return this.blockchain.size(); - } - - @Override - public int getATCreationBlockHeight(MachineState state) { - return 1; - } - - @Override - public void putPreviousBlockHashInA(MachineState state) { - this.setA1(state, this.blockchain.size() - 1); - this.setA2(state, state.getA1()); - this.setA3(state, state.getA1()); - this.setA4(state, state.getA1()); - } - - @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 - this.setA1(state, new Timestamp(blockHeight, transactionSequence).longValue()); - this.setA2(state, state.getA1()); - this.setA3(state, state.getA1()); - this.setA4(state, state.getA1()); - return; - } - - ++transactionSequence; - } - - // Nothing found - this.setA1(state, 0L); - this.setA2(state, 0L); - this.setA3(state, 0L); - this.setA4(state, 0L); - } - - @Override - public long getTypeFromTransactionInA(MachineState state) { - Timestamp timestamp = new Timestamp(state.getA1()); - 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.getA1()); - 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.getA1()); - return timestamp.longValue(); - } - - @Override - public long generateRandomUsingTransactionInA(MachineState state) { - // NOT USED - return 0L; - } - - @Override - public void putMessageFromTransactionInAIntoB(MachineState state) { - Timestamp timestamp = new Timestamp(state.getA1()); - Block block = this.blockchain.get(timestamp.blockHeight - 1); - Transaction transaction = block.transactions.get(timestamp.transactionSequence); - this.setB1(state, transaction.message[0]); - this.setB2(state, transaction.message[1]); - this.setB3(state, transaction.message[2]); - this.setB4(state, transaction.message[3]); - } - - @Override - public void putAddressFromTransactionInAIntoB(MachineState state) { - Timestamp timestamp = new Timestamp(state.getA1()); - Block block = this.blockchain.get(timestamp.blockHeight - 1); - Transaction transaction = block.transactions.get(timestamp.transactionSequence); - this.setB1(state, transaction.creator.charAt(0)); - this.setB2(state, state.getB1()); - this.setB3(state, state.getB1()); - this.setB4(state, state.getB1()); - } - - @Override - public void putCreatorAddressIntoB(MachineState state) { - // Dummy creator - this.setB1(state, "C".charAt(0)); - this.setB2(state, state.getB1()); - this.setB3(state, state.getB1()); - this.setB4(state, state.getB1()); - } - - @Override - public long getCurrentBalance(MachineState state) { - return this.balanceAT; - } - - public void setCurrentBalance(long balance) { - this.balanceAT = balance; - System.out.println("New AT balance: " + balance); - } - - @Override - public void payAmountToB(long amount, MachineState state) { - // Determine recipient using first char in B1 - char firstChar = String.format("%c", (byte) state.getB1()).charAt(0); - Account recipient = this.accounts.values().stream().filter((account) -> account.address.charAt(0) == firstChar).findFirst().get(); - - // Simulate payment - recipient.balance += amount; - System.out.println("Paid " + amount + " to " + recipient.address + ", their balance now: " + recipient.balance); - - // For debugging, output our new balance - long balance = state.getCurrentBalance() - amount; - System.out.println("Our balance now: " + balance); - } - - @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 onFinished(long amount, MachineState state) { - System.out.println("Finished - refunding remaining to creator"); - - Account creator = this.accounts.get("Creator"); - creator.balance += amount; - System.out.println("Paid " + amount + " to " + creator.address + ", their balance now: " + creator.balance); - } - - @Override - public void onFatalError(MachineState state, ExecutionException e) { - System.out.println("Fatal error: " + e.getMessage()); - System.out.println("No error address set - will refund to creator and finish"); - } - - @Override - public void platformSpecificPreExecuteCheck(int paramCount, boolean returnValueExpected, MachineState state, short rawFunctionCode) - throws IllegalFunctionCodeException { - // NOT USED - } - - @Override - public void platformSpecificPostCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { - // NOT USED - } - -} diff --git a/Java/src/test/java/common/ExecutableTest.java b/Java/src/test/java/common/ExecutableTest.java index 6eda3fc..105241b 100644 --- a/Java/src/test/java/common/ExecutableTest.java +++ b/Java/src/test/java/common/ExecutableTest.java @@ -88,6 +88,10 @@ public abstract class ExecutableTest { api.bumpCurrentBlockHeight(); } while (!onceOnly && !state.getIsFinished()); + unwrapState(state); + } + + protected byte[] unwrapState(MachineState state) { // Ready for diagnosis byte[] stateBytes = state.toBytes(); @@ -98,6 +102,8 @@ public abstract class ExecutableTest { callStackSize = stateByteBuffer.getInt(CALL_STACK_OFFSET); userStackOffset = CALL_STACK_OFFSET + 4 + callStackSize; userStackSize = stateByteBuffer.getInt(userStackOffset); + + return stateBytes; } protected long getData(int address) { @@ -105,6 +111,11 @@ public abstract class ExecutableTest { return stateByteBuffer.getLong(index); } + protected void getDataBytes(int address, byte[] dest) { + int index = DATA_OFFSET + address * MachineState.VALUE_SIZE; + stateByteBuffer.slice().position(index).get(dest); + } + protected int getCallStackPosition() { return TestUtils.NUM_CALL_STACK_PAGES * MachineState.ADDRESS_SIZE - callStackSize; } diff --git a/Java/src/test/java/common/TestAPI.java b/Java/src/test/java/common/TestAPI.java index 155e49b..45e68ca 100644 --- a/Java/src/test/java/common/TestAPI.java +++ b/Java/src/test/java/common/TestAPI.java @@ -1,5 +1,13 @@ package common; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + import org.ciyam.at.API; import org.ciyam.at.ExecutionException; import org.ciyam.at.FunctionData; @@ -10,29 +18,192 @@ import org.ciyam.at.Timestamp; public class TestAPI extends API { - private static final int BLOCK_PERIOD = 10 * 60; // average period between blocks in seconds + /** Average period between blocks, in seconds. */ + public static final int BLOCK_PERIOD = 10 * 60; + /** Maximum number of steps before auto-sleep. */ + public static final int MAX_STEPS_PER_ROUND = 500; + /** Op-code step multiplier for calling functions. */ + public static final int STEPS_PER_FUNCTION_CALL = 10; + + /** Initial balance for simple test scenarios. */ + public static final long DEFAULT_INITIAL_BALANCE = 1234L; + /** Initial block height for simple test scenarios. */ + public static final int DEFAULT_INITIAL_BLOCK_HEIGHT = 10; + /** AT creation block height for simple test scenarios. */ + public static final int DEFAULT_AT_CREATION_BLOCK_HEIGHT = 8; + + public static final String AT_CREATOR_ADDRESS = "AT Creator"; + public static final String AT_ADDRESS = "AT"; + + private static final Random RANDOM = new Random(); + + public static class TestAccount { + public String address; + public long balance; + public List messages = new ArrayList<>(); + + public TestAccount(String address, long amount) { + this.address = address; + this.balance = amount; + } + + public void addToMap(Map map) { + map.put(this.address, this); + } + } + + public static class TestTransaction { + public long timestamp; // block height & sequence + public byte[] txHash; + public API.ATTransactionType txType; + public String sender; + public String recipient; + public long amount; + public byte[] message; + + private TestTransaction(byte[] txHash, API.ATTransactionType txType, String creator, String recipient) { + this.txHash = txHash; + this.txType = txType; + this.sender = creator; + this.recipient = recipient; + } + + public TestTransaction(byte[] txHash, String creator, String recipient, long amount) { + this(txHash, API.ATTransactionType.PAYMENT, creator, recipient); + + this.amount = amount; + } + + public TestTransaction(byte[] txHash, String creator, String recipient, byte[] message) { + this(txHash, API.ATTransactionType.MESSAGE, creator, recipient); + + this.message = new byte[32]; + System.arraycopy(message, 0, this.message, 0, message.length); + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + } + + public static class TestBlock { + public byte[] blockHash; + + public List transactions = new ArrayList(); + + public TestBlock() { + this.blockHash = new byte[32]; + RANDOM.nextBytes(this.blockHash); + } + + public TestBlock(byte[] blockHash) { + this.blockHash = new byte[32]; + System.arraycopy(this.blockHash, 0, blockHash, 0, blockHash.length); + } + } + + public List blockchain; + public Map accounts; + public Map transactions; private int currentBlockHeight; - private long currentBalance; public TestAPI() { - this.currentBlockHeight = 10; - this.currentBalance = 1234L; + this.currentBlockHeight = DEFAULT_INITIAL_BLOCK_HEIGHT; + + // Fill block chain from block 1 to initial height with empty blocks + blockchain = new ArrayList<>(); + for (int h = 1; h <= this.currentBlockHeight; ++h) + blockchain.add(new TestBlock()); + + // Set up test accounts + accounts = new HashMap<>(); + new TestAccount(AT_CREATOR_ADDRESS, 1000000L).addToMap(accounts); + new TestAccount(AT_ADDRESS, DEFAULT_INITIAL_BALANCE).addToMap(accounts); + new TestAccount("Initiator", 100000L).addToMap(accounts); + new TestAccount("Responder", 200000L).addToMap(accounts); + new TestAccount("Bystander", 300000L).addToMap(accounts); + + transactions = new HashMap<>(); } public void bumpCurrentBlockHeight() { ++this.currentBlockHeight; } + public void setCurrentBlockHeight(int blockHeight) { + if (blockHeight > blockchain.size()) + throw new IllegalStateException("Refusing to set current block height to beyond blockchain end"); + + this.currentBlockHeight = blockHeight; + } + + private void generateBlock(boolean withTransactions, boolean includeTransactionToAt) { + TestBlock newBlock = new TestBlock(); + blockchain.add(newBlock); + + if (!withTransactions) + return; + + TestAccount atAccount = accounts.get(AT_ADDRESS); + List senderAccounts = new ArrayList<>(accounts.values()); + List recipientAccounts = new ArrayList<>(accounts.values()); + if (!includeTransactionToAt) + recipientAccounts.remove(atAccount); + + boolean includesAtTransaction = false; + int transactionCount = 8 + RANDOM.nextInt(8); + for (int i = 0; i < transactionCount || includeTransactionToAt && !includesAtTransaction; ++i) { + // Pick random sender + TestAccount sender = senderAccounts.get(RANDOM.nextInt(senderAccounts.size())); + // Pick random recipient + TestAccount recipient = recipientAccounts.get(RANDOM.nextInt(recipientAccounts.size())); + // Pick random transaction type + API.ATTransactionType txType = API.ATTransactionType.valueOf(RANDOM.nextInt(2)); + // Generate tx hash + byte[] txHash = new byte[32]; + RANDOM.nextBytes(txHash); + + TestTransaction transaction; + if (txType == API.ATTransactionType.PAYMENT) { + long amount = RANDOM.nextInt(100); // small amounts + transaction = new TestTransaction(txHash, sender.address, recipient.address, amount); + } else { + byte[] message = new byte[32]; + RANDOM.nextBytes(message); + transaction = new TestTransaction(txHash, sender.address, recipient.address, message); + } + + transaction.timestamp = Timestamp.toLong(blockchain.size(), newBlock.transactions.size()); + transactions.put(new String(txHash, StandardCharsets.ISO_8859_1), transaction); + newBlock.transactions.add(transaction); + + if (recipient.address.equals(AT_ADDRESS)) + includesAtTransaction = true; + } + } + + public void generateEmptyBlock() { + generateBlock(false, false); + } + + public void generateBlockWithNonAtTransactions() { + generateBlock(true, false); + } + + public void generateBlockWithAtTransaction() { + generateBlock(true, true); + } + @Override public int getMaxStepsPerRound() { - return 500; + return MAX_STEPS_PER_ROUND; } @Override public int getOpCodeSteps(OpCode opcode) { if (opcode.value >= OpCode.EXT_FUN.value && opcode.value <= OpCode.EXT_FUN_RET_DAT_2.value) - return 10; + return STEPS_PER_FUNCTION_CALL; return 1; } @@ -49,39 +220,75 @@ public class TestAPI extends API { @Override public int getATCreationBlockHeight(MachineState state) { - return 5; + return DEFAULT_AT_CREATION_BLOCK_HEIGHT; } @Override - public void putPreviousBlockHashInA(MachineState state) { - this.setA1(state, 9L); - this.setA2(state, 9L); - this.setA3(state, 9L); - this.setA4(state, 9L); + public void putPreviousBlockHashIntoA(MachineState state) { + int previousBlockHeight = this.currentBlockHeight - 1; + this.setA(state, blockchain.get(previousBlockHeight - 1).blockHash); } @Override - public void putTransactionAfterTimestampInA(Timestamp timestamp, MachineState state) { - // Cycle through transactions: 1 -> 2 -> 3 -> 0 -> 1 ... - this.setA1(state, (timestamp.transactionSequence + 1) % 4); - this.setA2(state, state.getA1()); - this.setA3(state, state.getA1()); - this.setA4(state, state.getA1()); + public void putTransactionAfterTimestampIntoA(Timestamp timestamp, MachineState state) { + int blockHeight = timestamp.blockHeight; + int transactionSequence = timestamp.transactionSequence + 1; + + while (blockHeight <= this.currentBlockHeight) { + TestBlock 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; + } + + TestTransaction transaction = transactions.get(transactionSequence); + + if (transaction.recipient.equals("AT")) { + // Found a transaction + System.out.println("Found transaction at height " + blockHeight + " sequence " + transactionSequence); + + // Generate pseudo-hash of transaction + this.setA(state, transaction.txHash); + return; + } + + ++transactionSequence; + } + + // Nothing found + this.setA(state, new byte[32]); + } + + public TestTransaction getTransactionFromA(MachineState state) { + byte[] aBytes = state.getA(); + String txHashString = new String(aBytes, StandardCharsets.ISO_8859_1); // ISO_8859_1 for simplistic 8-bit encoding + return transactions.get(txHashString); } @Override public long getTypeFromTransactionInA(MachineState state) { - return 0L; + TestTransaction transaction = getTransactionFromA(state); + return transaction.txType.value; } @Override public long getAmountFromTransactionInA(MachineState state) { - return 123L; + TestTransaction transaction = getTransactionFromA(state); + if (transaction.txType != API.ATTransactionType.PAYMENT) + return 0L; + + return transaction.amount; } @Override public long getTimestampFromTransactionInA(MachineState state) { - return 1536227162000L; + TestTransaction transaction = getTransactionFromA(state); + return transaction.timestamp; } @Override @@ -106,46 +313,71 @@ public class TestAPI extends API { @Override public void putMessageFromTransactionInAIntoB(MachineState state) { - this.setB1(state, state.getA4()); - this.setB2(state, state.getA3()); - this.setB3(state, state.getA2()); - this.setB4(state, state.getA1()); + TestTransaction transaction = getTransactionFromA(state); + if (transaction.txType != API.ATTransactionType.MESSAGE) + return; + + this.setB(state, transaction.message); } @Override public void putAddressFromTransactionInAIntoB(MachineState state) { - // Dummy address - this.setB1(state, 0xaaaaaaaaaaaaaaaaL); - this.setB2(state, 0xaaaaaaaaaaaaaaaaL); - this.setB3(state, 0xaaaaaaaaaaaaaaaaL); - this.setB4(state, 0xaaaaaaaaaaaaaaaaL); + TestTransaction transaction = getTransactionFromA(state); + byte[] bBytes = new byte[32]; + System.arraycopy(transaction.sender.getBytes(), 0, bBytes, 0, transaction.sender.length()); + this.setB(state, bBytes); } @Override public void putCreatorAddressIntoB(MachineState state) { - // Dummy creator - this.setB1(state, 0xccccccccccccccccL); - this.setB2(state, 0xccccccccccccccccL); - this.setB3(state, 0xccccccccccccccccL); - this.setB4(state, 0xccccccccccccccccL); + byte[] bBytes = new byte[32]; + System.arraycopy(AT_CREATOR_ADDRESS.getBytes(), 0, bBytes, 0, AT_CREATOR_ADDRESS.length()); + this.setB(state, bBytes); } @Override public long getCurrentBalance(MachineState state) { - return this.currentBalance; + return this.accounts.get(AT_ADDRESS).balance; } // Debugging only public void setCurrentBalance(long currentBalance) { - this.currentBalance = currentBalance; + this.accounts.get(AT_ADDRESS).balance = currentBalance; + System.out.println(String.format("New AT balance: %s", prettyAmount(currentBalance))); } @Override public void payAmountToB(long amount, MachineState state) { + if (amount <= 0) + return; + + byte[] bBytes = state.getB(); + String address = new String(bBytes, StandardCharsets.ISO_8859_1); + address = address.replace("\0", ""); + + TestAccount recipient = accounts.get(address); + if (recipient == null) + throw new IllegalStateException("Refusing to pay to unknown account: " + address); + + System.out.println(String.format("Paid %s to %s, their balance now: %s", prettyAmount(amount), recipient.address, recipient.balance)); + recipient.balance += amount; + + TestAccount atAccount = accounts.get(AT_ADDRESS); + atAccount.balance -= amount; + System.out.println(String.format("AT balance now: %s", atAccount.balance)); } @Override public void messageAToB(MachineState state) { + byte[] bBytes = state.getB(); + String address = new String(bBytes, StandardCharsets.ISO_8859_1); + address = address.replace("\0", ""); + + TestAccount recipient = accounts.get(address); + if (recipient == null) + throw new IllegalStateException("Refusing to send message to unknown account: " + address); + + recipient.messages.add(state.getA()); } @Override @@ -157,6 +389,12 @@ public class TestAPI extends API { @Override public void onFinished(long amount, MachineState state) { System.out.println("Finished - refunding remaining to creator"); + + TestAccount atCreatorAccount = accounts.get(AT_CREATOR_ADDRESS); + atCreatorAccount.balance += amount; + System.out.println(String.format("Paid %s to creator %s, their balance now: %s", prettyAmount(amount), atCreatorAccount.address, atCreatorAccount.balance)); + + accounts.get(AT_ADDRESS).balance -= amount; } @Override @@ -219,4 +457,8 @@ public class TestAPI extends API { } } + public static String prettyAmount(long amount) { + return BigDecimal.valueOf(amount, 8).toPlainString(); + } + }