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