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(); + } + }