diff --git a/Java/src/test/java/BlockchainFunctionCodeTests.java b/Java/src/test/java/BlockchainFunctionCodeTests.java index fa94e99..f340d64 100644 --- a/Java/src/test/java/BlockchainFunctionCodeTests.java +++ b/Java/src/test/java/BlockchainFunctionCodeTests.java @@ -1,15 +1,19 @@ import static org.junit.Assert.*; import java.util.Arrays; +import java.util.Random; +import org.ciyam.at.API; import org.ciyam.at.ExecutionException; import org.ciyam.at.FunctionCode; +import org.ciyam.at.MachineState; import org.ciyam.at.OpCode; import org.ciyam.at.Timestamp; import org.junit.Test; import common.ExecutableTest; import common.TestAPI; +import common.TestAPI.TestAccount; import common.TestAPI.TestBlock; import common.TestAPI.TestTransaction; @@ -131,13 +135,20 @@ public class BlockchainFunctionCodeTests extends ExecutableTest { dataByteBuffer.putLong(initialTimestamp); // Generate some blocks containing transactions (but none to AT) - api.generateBlockWithNonAtTransactions(); - api.generateBlockWithNonAtTransactions(); + TestBlock newBlock = api.generateBlockWithNonAtTransactions(); + api.addBlockToChain(newBlock); + api.bumpCurrentBlockHeight(); + + newBlock = api.generateBlockWithNonAtTransactions(); + api.addBlockToChain(newBlock); + api.bumpCurrentBlockHeight(); + // Generate a block containing transaction to AT - api.generateBlockWithAtTransaction(); + newBlock = api.generateBlockWithAtTransaction(); + api.addBlockToChain(newBlock); + api.bumpCurrentBlockHeight(); - int currentBlockHeight = api.blockchain.size(); - api.setCurrentBlockHeight(currentBlockHeight); + int currentBlockHeight = api.getCurrentBlockHeight(); // 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); @@ -162,9 +173,12 @@ public class BlockchainFunctionCodeTests extends ExecutableTest { dataByteBuffer.putLong(initialTimestamp); // Generate a block containing transaction to AT - api.generateBlockWithAtTransaction(); + TestBlock newBlock = api.generateBlockWithAtTransaction(); + api.addBlockToChain(newBlock); api.bumpCurrentBlockHeight(); - api.generateBlockWithAtTransaction(); + + newBlock = api.generateBlockWithAtTransaction(); + api.addBlockToChain(newBlock); api.bumpCurrentBlockHeight(); long expectedTransactionsCount = 0; @@ -199,6 +213,100 @@ public class BlockchainFunctionCodeTests extends ExecutableTest { assertFalse(state.getHadFatalError()); } + @Test + public void testGetTypeFromTxInA() throws ExecutionException { + int initialBlockHeight = TestAPI.DEFAULT_INITIAL_BLOCK_HEIGHT; + long initialTimestamp = Timestamp.toLong(initialBlockHeight, 0); + dataByteBuffer.putLong(initialTimestamp); + + // Generate new block containing 2 transactions to AT, one PAYMENT, one MESSAGE + TestBlock newBlock = api.generateEmptyBlock(); + + String sender = "Bystander"; + String recipient = TestAPI.AT_ADDRESS; + + TestTransaction paymentTx = api.generateTransaction(sender, recipient, API.ATTransactionType.PAYMENT); + newBlock.transactions.add(paymentTx); + + TestTransaction messageTx = api.generateTransaction(sender, recipient, API.ATTransactionType.MESSAGE); + newBlock.transactions.add(messageTx); + + api.addBlockToChain(newBlock); + api.bumpCurrentBlockHeight(); + + // Get transaction after timestamp held in address 0 + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A.value).putInt(0); + // Save transaction type into address 1 + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_TYPE_FROM_TX_IN_A.value).putInt(1); + // Update latest timestamp in address 0 + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A.value).putInt(0); + + // Get transaction after timestamp held in address 0 + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A.value).putInt(0); + // Save transaction type into address 2 + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_TYPE_FROM_TX_IN_A.value).putInt(2); + // Done + codeByteBuffer.put(OpCode.FIN_IMD.value); + + execute(true); + + long paymentTxType = getData(1); + assertEquals("Payment tx type mismatch", paymentTx.txType.value, paymentTxType); + + long messageTxType = getData(2); + assertEquals("Message tx type mismatch", messageTx.txType.value, messageTxType); + + assertTrue(state.getIsFinished()); + assertFalse(state.getHadFatalError()); + } + + @Test + public void testGetAmountFromTxInA() throws ExecutionException { + int initialBlockHeight = TestAPI.DEFAULT_INITIAL_BLOCK_HEIGHT; + long initialTimestamp = Timestamp.toLong(initialBlockHeight, 0); + dataByteBuffer.putLong(initialTimestamp); + + // Generate new block containing 2 transactions to AT, one PAYMENT, one MESSAGE + TestBlock newBlock = api.generateEmptyBlock(); + + String sender = "Bystander"; + String recipient = TestAPI.AT_ADDRESS; + + TestTransaction paymentTx = api.generateTransaction(sender, recipient, API.ATTransactionType.PAYMENT); + newBlock.transactions.add(paymentTx); + + TestTransaction messageTx = api.generateTransaction(sender, recipient, API.ATTransactionType.MESSAGE); + newBlock.transactions.add(messageTx); + + api.addBlockToChain(newBlock); + api.bumpCurrentBlockHeight(); + + // Get transaction after timestamp held in address 0 + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A.value).putInt(0); + // Save transaction's amount into address 1 + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_AMOUNT_FROM_TX_IN_A.value).putInt(1); + // Update latest timestamp in address 0 + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A.value).putInt(0); + + // Get transaction after timestamp held in address 0 + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A.value).putInt(0); + // Save transaction's amount into address 2 + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_AMOUNT_FROM_TX_IN_A.value).putInt(2); + // Done + codeByteBuffer.put(OpCode.FIN_IMD.value); + + execute(true); + + long paymentTxAmount = getData(1); + assertEquals("Payment tx amount mismatch", paymentTx.amount, paymentTxAmount); + + long messageTxAmount = getData(2); + assertEquals("Message tx amount mismatch", messageTx.amount, messageTxAmount); + + 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)); @@ -216,4 +324,474 @@ public class BlockchainFunctionCodeTests extends ExecutableTest { assertFalse(state.getHadFatalError()); } + @Test + public void testPutMessageFromTxInAIntoB() throws ExecutionException { + int initialBlockHeight = TestAPI.DEFAULT_INITIAL_BLOCK_HEIGHT; + long initialTimestamp = Timestamp.toLong(initialBlockHeight, 0); + dataByteBuffer.putLong(initialTimestamp); + + // Where to save message (in B) from payment tx + dataByteBuffer.putLong(4L); + + // Where to save message (in B) from message tx + dataByteBuffer.putLong(8L); + + // Generate new block containing 2 transactions to AT, one PAYMENT, one MESSAGE + TestBlock newBlock = api.generateEmptyBlock(); + + String sender = "Bystander"; + String recipient = TestAPI.AT_ADDRESS; + + TestTransaction paymentTx = api.generateTransaction(sender, recipient, API.ATTransactionType.PAYMENT); + newBlock.transactions.add(paymentTx); + + TestTransaction messageTx = api.generateTransaction(sender, recipient, API.ATTransactionType.MESSAGE); + newBlock.transactions.add(messageTx); + + api.addBlockToChain(newBlock); + api.bumpCurrentBlockHeight(); + + // Get transaction after timestamp held in address 0 + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A.value).putInt(0); + // Save transaction's message into addresses 4 to 7 + codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B.value); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.GET_B_IND.value).putInt(1); + // Update latest timestamp in address 0 + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A.value).putInt(0); + + // Get transaction after timestamp held in address 0 + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A.value).putInt(0); + // Save transaction's message into addresses 4 to 7 + codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B.value); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.GET_B_IND.value).putInt(2); + // Done + codeByteBuffer.put(OpCode.FIN_IMD.value); + + execute(true); + + byte[] actualMessage = new byte[32]; + + byte[] expectedMessage = new byte[32]; // Blank for non-message transactions + getDataBytes(4, actualMessage); + assertTrue("Payment tx message mismatch", Arrays.equals(expectedMessage, actualMessage)); + + expectedMessage = messageTx.message; + getDataBytes(8, actualMessage); + assertTrue("Message tx message mismatch", Arrays.equals(expectedMessage, actualMessage)); + + assertTrue(state.getIsFinished()); + assertFalse(state.getHadFatalError()); + } + + @Test + public void testPutAddressFromTxInAIntoB() throws ExecutionException { + int initialBlockHeight = TestAPI.DEFAULT_INITIAL_BLOCK_HEIGHT; + long initialTimestamp = Timestamp.toLong(initialBlockHeight, 0); + dataByteBuffer.putLong(initialTimestamp); + + // Where to save address (in B) from tx + dataByteBuffer.putLong(4L); + + // Generate new block containing a transaction to AT + TestBlock newBlock = api.generateEmptyBlock(); + + String sender = "Bystander"; + String recipient = TestAPI.AT_ADDRESS; + + TestTransaction messageTx = api.generateTransaction(sender, recipient, API.ATTransactionType.MESSAGE); + newBlock.transactions.add(messageTx); + + api.addBlockToChain(newBlock); + api.bumpCurrentBlockHeight(); + + // Get transaction after timestamp held in address 0 + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A.value).putInt(0); + // Save transaction's sender into addresses 4 to 7 + codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.PUT_ADDRESS_FROM_TX_IN_A_INTO_B.value); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.GET_B_IND.value).putInt(1); + // Done + codeByteBuffer.put(OpCode.FIN_IMD.value); + + execute(true); + + byte[] expectedSenderBytes = TestAPI.encodeAddress(sender); + byte[] actualSenderBytes = new byte[32]; + getDataBytes(4, actualSenderBytes); + assertTrue("Sender address bytes mismatch", Arrays.equals(expectedSenderBytes, actualSenderBytes)); + + String expectedSender = sender; + String actualSender = TestAPI.decodeAddress(actualSenderBytes); + assertEquals("Sender address string mismatch", expectedSender, actualSender); + + assertTrue(state.getIsFinished()); + assertFalse(state.getHadFatalError()); + } + + @Test + public void testPutCreatorIntoB() throws ExecutionException { + // Where to save creator address (in B) + dataByteBuffer.putLong(4L); + + // Save creator's address into data addresses 4 to 7 + codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.PUT_CREATOR_INTO_B.value); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.GET_B_IND.value).putInt(0); + // Done + codeByteBuffer.put(OpCode.FIN_IMD.value); + + execute(true); + + byte[] expectedAtCreatorBytes = TestAPI.encodeAddress(TestAPI.AT_CREATOR_ADDRESS); + byte[] actualAtCreatorBytes = new byte[32]; + getDataBytes(4, actualAtCreatorBytes); + assertTrue("AT creator address bytes mismatch", Arrays.equals(expectedAtCreatorBytes, actualAtCreatorBytes)); + + String expectedAtCreator = TestAPI.AT_CREATOR_ADDRESS; + String actualAtCreator = TestAPI.decodeAddress(actualAtCreatorBytes); + assertEquals("AT creator address string mismatch", expectedAtCreator, actualAtCreator); + + assertTrue(state.getIsFinished()); + assertFalse(state.getHadFatalError()); + } + + @Test + public void testGetCurrentBalance() throws ExecutionException { + // Number of bits to shift right + dataByteBuffer.putLong(1L); + + // Save current balance into address 1 + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_CURRENT_BALANCE.value).putInt(1); + // Copy balance from address 1 into address 2 + codeByteBuffer.put(OpCode.SET_DAT.value).putInt(2).putInt(1); + // Halve balance in address 2 + codeByteBuffer.put(OpCode.SHR_DAT.value).putInt(2).putInt(0); + // Pay amount in address 2 to creator (via B) + codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.PUT_CREATOR_INTO_B.value); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.PAY_TO_ADDRESS_IN_B.value).putInt(2); + // Save new current balance into address 3 + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_CURRENT_BALANCE.value).putInt(3); + // Done + codeByteBuffer.put(OpCode.FIN_IMD.value); + + final long initialBalance = api.accounts.get(TestAPI.AT_ADDRESS).balance; + + execute(true); + + long expectedBalance = initialBalance - TestAPI.STEPS_PER_FUNCTION_CALL /* GET_CURRENT_BALANCE */; + assertEquals("Initial 'current balance' mismatch", expectedBalance, getData(1)); + + final long amount = expectedBalance >>> 1; + expectedBalance -= 1 /* SET_DAT */ + + 1 /* SHR_DAT */ + + TestAPI.STEPS_PER_FUNCTION_CALL /* PUT_CREATOR_INTO_B */ + + TestAPI.STEPS_PER_FUNCTION_CALL /* PAY_TO_ADDRESS_IN_B */ + + TestAPI.STEPS_PER_FUNCTION_CALL /* GET_CURRENT_BALANCE */ + + amount; + + assertEquals("Final 'current balance' mismatch", expectedBalance, getData(3)); + + assertTrue(state.getIsFinished()); + assertFalse(state.getHadFatalError()); + } + + @Test + public void testGetPreviousBalance() throws ExecutionException { + // Save previous balance into address 1 + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_PREVIOUS_BALANCE.value).putInt(1); + // Done + codeByteBuffer.put(OpCode.STP_IMD.value); + + final long initialBalance = api.accounts.get(TestAPI.AT_ADDRESS).balance; + + execute(true); + + long expectedBalance = initialBalance; + assertEquals("Initial 'previous balance' mismatch", expectedBalance, getData(1)); + + execute(true); + + expectedBalance -= 1 /* STP_IMD */ + TestAPI.STEPS_PER_FUNCTION_CALL /* GET_CURRENT_BALANCE */; + + assertEquals("Final 'previous balance' mismatch", expectedBalance, getData(1)); + + assertFalse(state.getHadFatalError()); + } + + @Test + public void testPayToAddressInB() throws ExecutionException { + final long amount = 123L; + TestAccount recipient = api.accounts.get("Bystander"); + + // Where recipient address is stored + final long addressPosition = 2L; + dataByteBuffer.putLong(addressPosition); + + // Amount to send to recipient + dataByteBuffer.putLong(amount); + + // Recipient address + assertEquals(addressPosition * MachineState.VALUE_SIZE, dataByteBuffer.position()); + dataByteBuffer.put(TestAPI.encodeAddress(recipient.address)); + + // Copy recipient address into B + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B_IND.value).putInt(0); + // Pay amount in address 1 to recipient (via B) + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.PAY_TO_ADDRESS_IN_B.value).putInt(1); + // STOP, not finish, so we retain balance instead of sending leftover to AT creator + codeByteBuffer.put(OpCode.STP_IMD.value); + + final long initialAtBalance = api.accounts.get(TestAPI.AT_ADDRESS).balance; + final long initialRecipientBalance = recipient.balance; + + execute(true); + + long expectedBalance = initialAtBalance - amount; + long actualBalance = api.accounts.get(TestAPI.AT_ADDRESS).balance; + assertTrue("Final AT balance mismatch", actualBalance <= expectedBalance && actualBalance > 0); + + expectedBalance = initialRecipientBalance + amount; + actualBalance = recipient.balance; + assertEquals("Final recipient balance mismatch", expectedBalance, actualBalance); + + assertFalse(state.getHadFatalError()); + } + + @Test + public void testPayToAddressInBexcessive() throws ExecutionException { + final long amount = 999999999L; // More than AT's balance + TestAccount recipient = api.accounts.get("Bystander"); + + // Where recipient address is stored + final long addressPosition = 2L; + dataByteBuffer.putLong(addressPosition); + + // Amount to send to recipient + dataByteBuffer.putLong(amount); + + // Recipient address + assertEquals(addressPosition * MachineState.VALUE_SIZE, dataByteBuffer.position()); + dataByteBuffer.put(TestAPI.encodeAddress(recipient.address)); + + // Copy recipient address into B + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B_IND.value).putInt(0); + // Pay amount in address 1 to recipient (via B) + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.PAY_TO_ADDRESS_IN_B.value).putInt(1); + // STOP, not finish, so we retain balance instead of sending leftover to AT creator + codeByteBuffer.put(OpCode.STP_IMD.value); + + final long initialAtBalance = api.accounts.get(TestAPI.AT_ADDRESS).balance; + final long initialRecipientBalance = recipient.balance; + + execute(true); + + long expectedAmount = initialAtBalance + - TestAPI.STEPS_PER_FUNCTION_CALL /* SET_B_IND */ + - TestAPI.STEPS_PER_FUNCTION_CALL /* PAY_TO_ADDRESS_IN_B */; + + long expectedBalance = 0L; + long actualBalance = api.accounts.get(TestAPI.AT_ADDRESS).balance; + assertEquals("Final AT balance mismatch", expectedBalance, actualBalance); + + expectedBalance = initialRecipientBalance + expectedAmount; + actualBalance = recipient.balance; + assertEquals("Final recipient balance mismatch", expectedBalance, actualBalance); + + assertFalse(state.getHadFatalError()); + } + + @Test + public void testPayAllToAddressInB() throws ExecutionException { + TestAccount recipient = api.accounts.get("Bystander"); + + // Where recipient address is stored + final long addressPosition = 1L; + dataByteBuffer.putLong(addressPosition); + + // Recipient address + assertEquals(addressPosition * MachineState.VALUE_SIZE, dataByteBuffer.position()); + dataByteBuffer.put(TestAPI.encodeAddress(recipient.address)); + + // Copy recipient address into B + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B_IND.value).putInt(0); + // Pay all amount to recipient (via B) + codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.PAY_ALL_TO_ADDRESS_IN_B.value); + // STOP, not finish, so we retain balance instead of sending leftover to AT creator + codeByteBuffer.put(OpCode.STP_IMD.value); + + final long initialAtBalance = api.accounts.get(TestAPI.AT_ADDRESS).balance; + final long initialRecipientBalance = recipient.balance; + + execute(true); + + long expectedAmount = initialAtBalance + - TestAPI.STEPS_PER_FUNCTION_CALL /* SET_B_IND */ + - TestAPI.STEPS_PER_FUNCTION_CALL /* PAY_ALL_TO_ADDRESS_IN_B */; + + long expectedBalance = 0L; + long actualBalance = api.accounts.get(TestAPI.AT_ADDRESS).balance; + assertEquals("Final AT balance mismatch", expectedBalance, actualBalance); + + expectedBalance = initialRecipientBalance + expectedAmount; + actualBalance = recipient.balance; + assertEquals("Final recipient balance mismatch", expectedBalance, actualBalance); + + assertFalse(state.getHadFatalError()); + } + + @Test + public void testPayPreviousToAddressInB() throws ExecutionException { + TestAccount recipient = api.accounts.get("Bystander"); + + // Where recipient address is stored + final long addressPosition = 1L; + dataByteBuffer.putLong(addressPosition); + + // Recipient address + assertEquals(addressPosition * MachineState.VALUE_SIZE, dataByteBuffer.position()); + dataByteBuffer.put(TestAPI.encodeAddress(recipient.address)); + + // Copy recipient address into B + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B_IND.value).putInt(0); + // Pay previous balance to recipient (via B) + codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.PAY_PREVIOUS_TO_ADDRESS_IN_B.value); + // STOP, not finish, so we retain balance instead of sending leftover to AT creator + codeByteBuffer.put(OpCode.STP_IMD.value); + + final long initialAtBalance = api.accounts.get(TestAPI.AT_ADDRESS).balance; + final long initialRecipientBalance = recipient.balance; + + execute(true); + + long expectedAmount = initialAtBalance + - TestAPI.STEPS_PER_FUNCTION_CALL /* SET_B_IND */ + - TestAPI.STEPS_PER_FUNCTION_CALL /* PAY_PREVIOUS_TO_ADDRESS_IN_B */; + + long expectedBalance = 0L; + long actualBalance = api.accounts.get(TestAPI.AT_ADDRESS).balance; + assertEquals("Final AT balance mismatch", expectedBalance, actualBalance); + + expectedBalance = initialRecipientBalance + expectedAmount; + actualBalance = recipient.balance; + assertEquals("Final recipient balance mismatch", expectedBalance, actualBalance); + + assertFalse(state.getHadFatalError()); + } + + @Test + public void testPayPreviousToAddressInBextra() throws ExecutionException { + TestAccount recipient = api.accounts.get("Bystander"); + + // Where recipient address is stored + final long addressPosition = 1L; + dataByteBuffer.putLong(addressPosition); + + // Recipient address + assertEquals(addressPosition * MachineState.VALUE_SIZE, dataByteBuffer.position()); + dataByteBuffer.put(TestAPI.encodeAddress(recipient.address)); + + // Sleep until next block + codeByteBuffer.put(OpCode.SLP_IMD.value); + // Copy recipient address into B + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B_IND.value).putInt(0); + // Pay previous balance to recipient (via B) + codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.PAY_PREVIOUS_TO_ADDRESS_IN_B.value); + // STOP, not finish, so we retain balance instead of sending leftover to AT creator + codeByteBuffer.put(OpCode.STP_IMD.value); + + execute(true); + + final TestAccount atAccount = api.accounts.get(TestAPI.AT_ADDRESS); + + final long previousAtBalance = atAccount.balance; + final long initialRecipientBalance = recipient.balance; + + // Simulate AT receiving a payment (in excess of cost of running AT for one round) + final long incomingAtPayment = 25000L; + atAccount.balance += incomingAtPayment; + + execute(true); + + long expectedBalance = incomingAtPayment + - TestAPI.STEPS_PER_FUNCTION_CALL /* SET_B_IND */ + - TestAPI.STEPS_PER_FUNCTION_CALL /* PAY_PREVIOUS_TO_ADDRESS_IN_B */ + - 1 /* STP_IMD */; + long actualBalance = atAccount.balance; + assertEquals("Final AT balance mismatch", expectedBalance, actualBalance); + + expectedBalance = initialRecipientBalance + previousAtBalance; + actualBalance = recipient.balance; + assertEquals("Final recipient balance mismatch", expectedBalance, actualBalance); + + assertFalse(state.getHadFatalError()); + } + + @Test + public void testMessageAToAddressInB() throws ExecutionException { + Random random = new Random(); + + byte[] message = new byte[32]; + random.nextBytes(message); + + TestAccount recipient = api.accounts.get("Bystander"); + + // Where message is stored + final long messagePosition = 2L; + dataByteBuffer.putLong(messagePosition); + + // Where recipient address is stored + final long addressPosition = 6L; + dataByteBuffer.putLong(addressPosition); + + // Message + assertEquals(messagePosition * MachineState.VALUE_SIZE, dataByteBuffer.position()); + dataByteBuffer.put(message); + + // Recipient address + assertEquals(addressPosition * MachineState.VALUE_SIZE, dataByteBuffer.position()); + dataByteBuffer.put(TestAPI.encodeAddress(recipient.address)); + + // Copy message into A + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_A_IND.value).putInt(0); + // Copy recipient address into B + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B_IND.value).putInt(1); + // Send message (in A) to recipient (via B) + codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.MESSAGE_A_TO_ADDRESS_IN_B.value); + // Done + codeByteBuffer.put(OpCode.FIN_IMD.value); + + execute(true); + + assertEquals("Recipient message count incorrect", 1, recipient.messages.size()); + + byte[] actualMessage = recipient.messages.get(0); + assertTrue("Recipient message incorrect", Arrays.equals(message, actualMessage)); + + assertTrue(state.getIsFinished()); + assertFalse(state.getHadFatalError()); + } + + @Test + public void testAddMinutesToTimestamp() throws ExecutionException { + int initialBlockHeight = TestAPI.DEFAULT_INITIAL_BLOCK_HEIGHT; + long initialTimestamp = Timestamp.toLong(initialBlockHeight, 0); + dataByteBuffer.putLong(initialTimestamp); + + long minutes = 34L; + dataByteBuffer.putLong(minutes); + + // Add minutes (from address 1) to timestamp (in address 0) and store the result in address 2 + codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.value).putShort(FunctionCode.ADD_MINUTES_TO_TIMESTAMP.value).putInt(2).putInt(0).putInt(1); + // Done + codeByteBuffer.put(OpCode.FIN_IMD.value); + + execute(true); + + int expectedBlockHeight = initialBlockHeight + ((int) minutes * 60 / TestAPI.BLOCK_PERIOD); + long expectedTimestamp = Timestamp.toLong(expectedBlockHeight, 0); + long actualTimestamp = getData(2); + assertEquals("Expected timestamp incorrect", expectedTimestamp, actualTimestamp); + + assertTrue(state.getIsFinished()); + assertFalse(state.getHadFatalError()); + } + } diff --git a/Java/src/test/java/common/TestAPI.java b/Java/src/test/java/common/TestAPI.java index 45e68ca..4728efc 100644 --- a/Java/src/test/java/common/TestAPI.java +++ b/Java/src/test/java/common/TestAPI.java @@ -61,21 +61,21 @@ public class TestAPI extends API { public long amount; public byte[] message; - private TestTransaction(byte[] txHash, API.ATTransactionType txType, String creator, String recipient) { + private TestTransaction(byte[] txHash, API.ATTransactionType txType, String sender, String recipient) { this.txHash = txHash; this.txType = txType; - this.sender = creator; + this.sender = sender; this.recipient = recipient; } - public TestTransaction(byte[] txHash, String creator, String recipient, long amount) { - this(txHash, API.ATTransactionType.PAYMENT, creator, recipient); + public TestTransaction(byte[] txHash, String sender, String recipient, long amount) { + this(txHash, API.ATTransactionType.PAYMENT, sender, recipient); this.amount = amount; } - public TestTransaction(byte[] txHash, String creator, String recipient, byte[] message) { - this(txHash, API.ATTransactionType.MESSAGE, creator, recipient); + public TestTransaction(byte[] txHash, String sender, String recipient, byte[] message) { + this(txHash, API.ATTransactionType.MESSAGE, sender, recipient); this.message = new byte[32]; System.arraycopy(message, 0, this.message, 0, message.length); @@ -127,6 +127,21 @@ public class TestAPI extends API { transactions = new HashMap<>(); } + public static byte[] encodeAddress(String address) { + byte[] encodedAddress = new byte[32]; + System.arraycopy(address.getBytes(), 0, encodedAddress, 0, address.length()); + return encodedAddress; + } + + public static String decodeAddress(byte[] encodedAddress) { + String address = new String(encodedAddress, StandardCharsets.ISO_8859_1); + return address.replace("\0", ""); + } + + public static String stringifyHash(byte[] hash) { + return new String(hash, StandardCharsets.ISO_8859_1); + } + public void bumpCurrentBlockHeight() { ++this.currentBlockHeight; } @@ -138,12 +153,28 @@ public class TestAPI extends API { this.currentBlockHeight = blockHeight; } - private void generateBlock(boolean withTransactions, boolean includeTransactionToAt) { - TestBlock newBlock = new TestBlock(); + public TestBlock addBlockToChain(TestBlock newBlock) { blockchain.add(newBlock); + final int blockHeight = blockchain.size(); + + for (int seq = 0; seq < newBlock.transactions.size(); ++seq) { + TestTransaction transaction = newBlock.transactions.get(seq); + + // Set transaction timestamp + transaction.timestamp = Timestamp.toLong(blockHeight, seq); + + // Add to transactions map + transactions.put(stringifyHash(transaction.txHash), transaction); + } + + return newBlock; + } + + private TestBlock generateBlock(boolean withTransactions, boolean includeTransactionToAt) { + TestBlock newBlock = new TestBlock(); if (!withTransactions) - return; + return newBlock; TestAccount atAccount = accounts.get(AT_ADDRESS); List senderAccounts = new ArrayList<>(accounts.values()); @@ -160,39 +191,46 @@ public class TestAPI extends API { 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); + TestTransaction transaction = generateTransaction(sender.address, recipient.address, txType); newBlock.transactions.add(transaction); if (recipient.address.equals(AT_ADDRESS)) includesAtTransaction = true; } + + return newBlock; } - public void generateEmptyBlock() { - generateBlock(false, false); + public TestBlock generateEmptyBlock() { + return generateBlock(false, false); } - public void generateBlockWithNonAtTransactions() { - generateBlock(true, false); + public TestBlock generateBlockWithNonAtTransactions() { + return generateBlock(true, false); } - public void generateBlockWithAtTransaction() { - generateBlock(true, true); + public TestBlock generateBlockWithAtTransaction() { + return generateBlock(true, true); + } + + public TestTransaction generateTransaction(String sender, String recipient, API.ATTransactionType txType) { + TestTransaction transaction; + + // Generate tx hash + byte[] txHash = new byte[32]; + RANDOM.nextBytes(txHash); + + if (txType == API.ATTransactionType.PAYMENT) { + long amount = RANDOM.nextInt(100); // small amounts + transaction = new TestTransaction(txHash, sender, recipient, amount); + } else { + byte[] message = new byte[32]; + RANDOM.nextBytes(message); + transaction = new TestTransaction(txHash, sender, recipient, message); + } + + return transaction; } @Override @@ -239,7 +277,7 @@ public class TestAPI extends API { List transactions = block.transactions; - if (transactionSequence > transactions.size() - 1) { + if (transactionSequence >= transactions.size()) { // No more transactions at this height ++blockHeight; transactionSequence = 0; @@ -261,12 +299,13 @@ public class TestAPI extends API { } // Nothing found + System.out.println("No more transactions found at height " + this.currentBlockHeight); 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 + String txHashString = stringifyHash(aBytes); return transactions.get(txHashString); } @@ -323,15 +362,13 @@ public class TestAPI extends API { @Override public void putAddressFromTransactionInAIntoB(MachineState state) { TestTransaction transaction = getTransactionFromA(state); - byte[] bBytes = new byte[32]; - System.arraycopy(transaction.sender.getBytes(), 0, bBytes, 0, transaction.sender.length()); + byte[] bBytes = encodeAddress(transaction.sender); this.setB(state, bBytes); } @Override public void putCreatorAddressIntoB(MachineState state) { - byte[] bBytes = new byte[32]; - System.arraycopy(AT_CREATOR_ADDRESS.getBytes(), 0, bBytes, 0, AT_CREATOR_ADDRESS.length()); + byte[] bBytes = encodeAddress(AT_CREATOR_ADDRESS); this.setB(state, bBytes); } @@ -348,30 +385,25 @@ public class TestAPI extends API { @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", ""); + String address = decodeAddress(bBytes); 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; + System.out.println(String.format("Paid %s to '%s', their balance now: %s", prettyAmount(amount), recipient.address, prettyAmount(recipient.balance))); - TestAccount atAccount = accounts.get(AT_ADDRESS); - atAccount.balance -= amount; - System.out.println(String.format("AT balance now: %s", atAccount.balance)); + final long previousBalance = state.getCurrentBalance(); + final long newBalance = previousBalance - amount; + System.out.println(String.format("AT balance was %s, now: %s", prettyAmount(previousBalance), prettyAmount(newBalance))); } @Override public void messageAToB(MachineState state) { byte[] bBytes = state.getB(); - String address = new String(bBytes, StandardCharsets.ISO_8859_1); - address = address.replace("\0", ""); + String address = decodeAddress(bBytes); TestAccount recipient = accounts.get(address); if (recipient == null) @@ -382,7 +414,7 @@ public class TestAPI extends API { @Override public long addMinutesToTimestamp(Timestamp timestamp, long minutes, MachineState state) { - timestamp.blockHeight = ((int) minutes * 60) / BLOCK_PERIOD; + timestamp.blockHeight += ((int) minutes * 60) / BLOCK_PERIOD; return timestamp.longValue(); } @@ -392,7 +424,7 @@ public class TestAPI extends API { 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)); + System.out.println(String.format("Paid %s to AT creator '%s', their balance now: %s", prettyAmount(amount), atCreatorAccount.address, prettyAmount(atCreatorAccount.balance))); accounts.get(AT_ADDRESS).balance -= amount; }