diff --git a/lib/org/ciyam/AT/1.3.4/AT-1.3.4.jar b/lib/org/ciyam/AT/1.3.4/AT-1.3.4.jar index 5abe2c77..611836fa 100644 Binary files a/lib/org/ciyam/AT/1.3.4/AT-1.3.4.jar and b/lib/org/ciyam/AT/1.3.4/AT-1.3.4.jar differ diff --git a/lib/org/ciyam/AT/maven-metadata-local.xml b/lib/org/ciyam/AT/maven-metadata-local.xml index 2cf6d13a..680b4f78 100644 --- a/lib/org/ciyam/AT/maven-metadata-local.xml +++ b/lib/org/ciyam/AT/maven-metadata-local.xml @@ -7,6 +7,6 @@ 1.3.4 - 20200414162728 + 20200609101009 diff --git a/src/main/java/org/qortal/at/QortalATAPI.java b/src/main/java/org/qortal/at/QortalATAPI.java index bf7d2abc..8c6e4ba9 100644 --- a/src/main/java/org/qortal/at/QortalATAPI.java +++ b/src/main/java/org/qortal/at/QortalATAPI.java @@ -37,6 +37,7 @@ import org.qortal.transaction.AtTransaction; import org.qortal.transaction.Transaction; import org.qortal.transaction.Transaction.TransactionType; import org.qortal.utils.Base58; +import org.qortal.utils.BitTwiddling; import com.google.common.primitives.Bytes; @@ -133,9 +134,9 @@ public class QortalATAPI extends API { byte[] signature = blockSummaries.get(0).getSignature(); // Save some of minter's signature and transactions signature, so middle 24 bytes of the full 128 byte signature. - this.setA2(state, fromBytes(signature, 52)); - this.setA3(state, fromBytes(signature, 60)); - this.setA4(state, fromBytes(signature, 68)); + this.setA2(state, BitTwiddling.longFromBEBytes(signature, 52)); + this.setA3(state, BitTwiddling.longFromBEBytes(signature, 60)); + this.setA4(state, BitTwiddling.longFromBEBytes(signature, 68)); } catch (DataException e) { throw new RuntimeException("AT API unable to fetch previous block?", e); } @@ -186,9 +187,9 @@ public class QortalATAPI extends API { // Copy transaction's partial signature into the other three A fields for future verification that it's the same transaction byte[] signature = transaction.getTransactionData().getSignature(); - this.setA2(state, fromBytes(signature, 8)); - this.setA3(state, fromBytes(signature, 16)); - this.setA4(state, fromBytes(signature, 24)); + this.setA2(state, BitTwiddling.longFromBEBytes(signature, 8)); + this.setA3(state, BitTwiddling.longFromBEBytes(signature, 16)); + this.setA4(state, BitTwiddling.longFromBEBytes(signature, 24)); return; } @@ -282,7 +283,7 @@ public class QortalATAPI extends API { byte[] hash = Crypto.digest(input); - return fromBytes(hash, 0); + return BitTwiddling.longFromBEBytes(hash, 0); } catch (DataException e) { throw new RuntimeException("AT API unable to fetch latest block from repository?", e); } @@ -296,20 +297,7 @@ public class QortalATAPI extends API { TransactionData transactionData = this.getTransactionFromA(state); - byte[] messageData = null; - - switch (transactionData.getType()) { - case MESSAGE: - messageData = ((MessageTransactionData) transactionData).getData(); - break; - - case AT: - messageData = ((ATTransactionData) transactionData).getMessage(); - break; - - default: - return; - } + byte[] messageData = this.getMessageFromTransaction(transactionData); // Check data length is appropriate, i.e. not larger than B if (messageData.length > 4 * 8) @@ -457,12 +445,6 @@ public class QortalATAPI extends API { // Utility methods - /** Convert part of little-endian byte[] to long */ - /* package */ static long fromBytes(byte[] bytes, int start) { - return (bytes[start] & 0xffL) | (bytes[start + 1] & 0xffL) << 8 | (bytes[start + 2] & 0xffL) << 16 | (bytes[start + 3] & 0xffL) << 24 - | (bytes[start + 4] & 0xffL) << 32 | (bytes[start + 5] & 0xffL) << 40 | (bytes[start + 6] & 0xffL) << 48 | (bytes[start + 7] & 0xffL) << 56; - } - /** Returns partial transaction signature, used to verify we're operating on the same transaction and not naively using block height & sequence. */ public static byte[] partialSignature(byte[] fullSignature) { return Arrays.copyOfRange(fullSignature, 8, 32); @@ -473,7 +455,7 @@ public class QortalATAPI extends API { // Compare end of transaction's signature against A2 thru A4 byte[] sig = transactionData.getSignature(); - if (this.getA2(state) != fromBytes(sig, 8) || this.getA3(state) != fromBytes(sig, 16) || this.getA4(state) != fromBytes(sig, 24)) + if (this.getA2(state) != BitTwiddling.longFromBEBytes(sig, 8) || this.getA3(state) != BitTwiddling.longFromBEBytes(sig, 16) || this.getA4(state) != BitTwiddling.longFromBEBytes(sig, 24)) throw new IllegalStateException("Transaction signature in A no longer matches signature from repository"); } @@ -497,6 +479,20 @@ public class QortalATAPI extends API { } } + /** Returns message data from transaction. */ + /*package*/ byte[] getMessageFromTransaction(TransactionData transactionData) { + switch (transactionData.getType()) { + case MESSAGE: + return ((MessageTransactionData) transactionData).getData(); + + case AT: + return ((ATTransactionData) transactionData).getMessage(); + + default: + return null; + } + } + /** Returns AT's account */ /* package */ Account getATAccount() { return new Account(this.repository, this.atData.getATAddress()); @@ -563,4 +559,8 @@ public class QortalATAPI extends API { super.setB(state, bBytes); } + protected void zeroB(MachineState state) { + super.zeroB(state); + } + } diff --git a/src/main/java/org/qortal/at/QortalFunctionCode.java b/src/main/java/org/qortal/at/QortalFunctionCode.java index cf6b1cfd..67ab5b98 100644 --- a/src/main/java/org/qortal/at/QortalFunctionCode.java +++ b/src/main/java/org/qortal/at/QortalFunctionCode.java @@ -12,6 +12,7 @@ import org.ciyam.at.IllegalFunctionCodeException; import org.ciyam.at.MachineState; import org.qortal.crosschain.BTC; import org.qortal.crypto.Crypto; +import org.qortal.data.transaction.TransactionData; import org.qortal.settings.Settings; /** @@ -22,8 +23,70 @@ import org.qortal.settings.Settings; */ public enum QortalFunctionCode { /** - * 0x0510
- * Convert address in B to 20-byte value in LSB of B1, and all of B2 & B3. + * Returns length of message data from transaction in A.
+ * 0x0501
+ * If transaction has no 'message', returns -1. + */ + GET_MESSAGE_LENGTH_FROM_TX_IN_A(0x0501, 0, true) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + QortalATAPI api = (QortalATAPI) state.getAPI(); + + TransactionData transactionData = api.getTransactionFromA(state); + + byte[] messageData = api.getMessageFromTransaction(transactionData); + + if (messageData == null) + functionData.returnValue = -1L; + else + functionData.returnValue = (long) messageData.length; + } + }, + /** + * Put offset 'message' from transaction in A into B
+ * 0x0502 start-offset
+ * Copies up to 32 bytes of message data, starting at start-offset into B.
+ * If transaction has no 'message', or start-offset out of bounds, then zero B
+ * Example 'message' could be 256-bit shared secret + */ + PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B(0x0502, 1, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + QortalATAPI api = (QortalATAPI) state.getAPI(); + + // In case something goes wrong, or we don't have enough message data. + api.zeroB(state); + + if (functionData.value1 < 0 || functionData.value1 > Integer.MAX_VALUE) + return; + + int startOffset = functionData.value1.intValue(); + + TransactionData transactionData = api.getTransactionFromA(state); + + byte[] messageData = api.getMessageFromTransaction(transactionData); + + if (messageData == null || startOffset > messageData.length) + return; + + /* + * Copy up to 32 bytes of message data into B, + * retain order but pad with zeros in lower bytes. + * + * So a 4-byte message "a b c d" would copy thusly: + * a b c d 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + */ + int byteCount = Math.min(32, messageData.length - startOffset); + byte[] bBytes = new byte[32]; + + System.arraycopy(messageData, startOffset, bBytes, 0, byteCount); + + api.setB(state, bBytes); + } + }, + /** + * Convert address in B to 20-byte value in LSB of B1, and all of B2 & B3.
+ * 0x0510 */ CONVERT_B_TO_PKH(0x0510, 0, false) { @Override @@ -38,8 +101,8 @@ public enum QortalFunctionCode { } }, /** - * 0x0511
* Convert 20-byte value in LSB of B1, and all of B2 & B3 to P2SH.
+ * 0x0511
* P2SH stored in lower 25 bytes of B. */ CONVERT_B_TO_P2SH(0x0511, 0, false) { @@ -51,8 +114,8 @@ public enum QortalFunctionCode { } }, /** - * 0x0512
* Convert 20-byte value in LSB of B1, and all of B2 & B3 to Qortal address.
+ * 0x0512
* Qortal address stored in lower 25 bytes of B. */ CONVERT_B_TO_QORTAL(0x0512, 0, false) { diff --git a/src/main/java/org/qortal/crosschain/BTC.java b/src/main/java/org/qortal/crosschain/BTC.java index ec53eb08..88428262 100644 --- a/src/main/java/org/qortal/crosschain/BTC.java +++ b/src/main/java/org/qortal/crosschain/BTC.java @@ -99,7 +99,7 @@ public class BTC { if (blockHeaders == null || blockHeaders.size() < 11) return null; - List blockTimestamps = blockHeaders.stream().map(blockHeader -> BitTwiddling.fromLEBytes(blockHeader, TIMESTAMP_OFFSET)).collect(Collectors.toList()); + List blockTimestamps = blockHeaders.stream().map(blockHeader -> BitTwiddling.intFromLEBytes(blockHeader, TIMESTAMP_OFFSET)).collect(Collectors.toList()); // Descending, but order shouldn't matter as we're picking median... blockTimestamps.sort((a, b) -> Integer.compare(b, a)); diff --git a/src/main/java/org/qortal/utils/BitTwiddling.java b/src/main/java/org/qortal/utils/BitTwiddling.java index f13300c5..4ba48bc8 100644 --- a/src/main/java/org/qortal/utils/BitTwiddling.java +++ b/src/main/java/org/qortal/utils/BitTwiddling.java @@ -27,8 +27,14 @@ public class BitTwiddling { } /** Convert little-endian bytes to int */ - public static int fromLEBytes(byte[] bytes, int offset) { + public static int intFromLEBytes(byte[] bytes, int offset) { return (bytes[offset] & 0xff) | (bytes[offset + 1] & 0xff) << 8 | (bytes[offset + 2] & 0xff) << 16 | (bytes[offset + 3] & 0xff) << 24; } + /** Convert big-endian bytes to long */ + public static long longFromBEBytes(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); + } + } diff --git a/src/test/java/org/qortal/test/at/GetMessageLengthTests.java b/src/test/java/org/qortal/test/at/GetMessageLengthTests.java new file mode 100644 index 00000000..730b441f --- /dev/null +++ b/src/test/java/org/qortal/test/at/GetMessageLengthTests.java @@ -0,0 +1,223 @@ +package org.qortal.test.at; + +import static org.junit.Assert.*; + +import java.nio.ByteBuffer; +import java.util.Random; + +import org.ciyam.at.CompilationException; +import org.ciyam.at.FunctionCode; +import org.ciyam.at.MachineState; +import org.ciyam.at.OpCode; +import org.junit.Before; +import org.junit.Test; +import org.qortal.account.PrivateKeyAccount; +import org.qortal.asset.Asset; +import org.qortal.at.QortalAtLoggerFactory; +import org.qortal.at.QortalFunctionCode; +import org.qortal.data.at.ATStateData; +import org.qortal.data.transaction.BaseTransactionData; +import org.qortal.data.transaction.DeployAtTransactionData; +import org.qortal.data.transaction.MessageTransactionData; +import org.qortal.data.transaction.TransactionData; +import org.qortal.group.Group; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; +import org.qortal.test.common.AccountUtils; +import org.qortal.test.common.BlockUtils; +import org.qortal.test.common.Common; +import org.qortal.test.common.TransactionUtils; +import org.qortal.transaction.DeployAtTransaction; +import org.qortal.transaction.MessageTransaction; +import org.qortal.utils.BitTwiddling; + +public class GetMessageLengthTests extends Common { + + private static final Random RANDOM = new Random(); + + @Before + public void before() throws DataException { + Common.useDefaultSettings(); + } + + @Test + public void testGetMessageLength() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); + + byte[] creationBytes = buildMessageLengthAT(); + + long fundingAmount = 1_00000000L; + DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount); + String atAddress = deployAtTransaction.getATAccount().getAddress(); + + // Send messages with known length + checkMessageLength(repository, deployer, atAddress, 1); + checkMessageLength(repository, deployer, atAddress, 10); + checkMessageLength(repository, deployer, atAddress, 32); + checkMessageLength(repository, deployer, atAddress, 99); + + // Finally, send a payment instead and check returned length is -1 + AccountUtils.pay(repository, deployer, atAddress, 123L); + // Mint another block so AT can process payment + BlockUtils.mintBlock(repository); + + // Check AT result + ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress); + byte[] stateData = atStateData.getStateData(); + + QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance(); + byte[] dataBytes = MachineState.extractDataBytes(loggerFactory, stateData); + + long extractedLength = BitTwiddling.longFromBEBytes(dataBytes, 0); + + assertEquals(-1L, extractedLength); + } + } + + private void checkMessageLength(Repository repository, PrivateKeyAccount sender, String atAddress, int messageLength) throws DataException { + byte[] testMessage = new byte[messageLength]; + RANDOM.nextBytes(testMessage); + + sendMessage(repository, sender, testMessage, atAddress); + // Mint another block so AT can process message + BlockUtils.mintBlock(repository); + + // Check AT result + ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress); + byte[] stateData = atStateData.getStateData(); + + QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance(); + byte[] dataBytes = MachineState.extractDataBytes(loggerFactory, stateData); + + long extractedLength = BitTwiddling.longFromBEBytes(dataBytes, 0); + + assertEquals(messageLength, extractedLength); + } + + private byte[] buildMessageLengthAT() { + // Labels for data segment addresses + int addrCounter = 0; + + // Make result first for easier extraction + final int addrResult = addrCounter++; + final int addrLastTxTimestamp = addrCounter++; + + // Data segment + ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE); + + // Code labels + Integer labelCheckTx = null; + + ByteBuffer codeByteBuffer = ByteBuffer.allocate(512); + + // Two-pass version + for (int pass = 0; pass < 2; ++pass) { + codeByteBuffer.clear(); + + try { + /* Initialization */ + + // Use AT creation 'timestamp' as starting point for finding transactions sent to AT + codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrLastTxTimestamp)); + + // Set restart position to after this opcode + codeByteBuffer.put(OpCode.SET_PCS.compile()); + + /* Loop, waiting for message to AT */ + + // Find next transaction to this AT since the last one (if any) + codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxTimestamp)); + // If no transaction found, A will be zero. If A is zero, set addrComparator to 1, otherwise 0. + codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.CHECK_A_IS_ZERO, addrResult)); + // If addrResult is zero (i.e. A is non-zero, transaction was found) then go check transaction + codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, OpCode.calcOffset(codeByteBuffer, labelCheckTx))); + // Stop and wait for next block + codeByteBuffer.put(OpCode.STP_IMD.compile()); + + /* Check transaction */ + labelCheckTx = codeByteBuffer.position(); + + // Update our 'last found transaction's timestamp' using 'timestamp' from transaction + codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A, addrLastTxTimestamp)); + // Save message length + codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(QortalFunctionCode.GET_MESSAGE_LENGTH_FROM_TX_IN_A.value, addrResult)); + + // Stop and wait for next block (and hence more transactions) + codeByteBuffer.put(OpCode.STP_IMD.compile()); + } catch (CompilationException e) { + throw new IllegalStateException("Unable to compile AT?", e); + } + } + + codeByteBuffer.flip(); + + byte[] codeBytes = new byte[codeByteBuffer.limit()]; + codeByteBuffer.get(codeBytes); + + final short ciyamAtVersion = 2; + final short numCallStackPages = 0; + final short numUserStackPages = 0; + final long minActivationAmount = 0L; + + return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount); + } + + private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, byte[] creationBytes, long fundingAmount) throws DataException { + long txTimestamp = System.currentTimeMillis(); + byte[] lastReference = deployer.getLastReference(); + + if (lastReference == null) { + System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress())); + System.exit(2); + } + + Long fee = null; + String name = "Test AT"; + String description = "Test AT"; + String atType = "Test"; + String tags = "TEST"; + + BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null); + TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT); + + DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData); + + fee = deployAtTransaction.calcRecommendedFee(); + deployAtTransactionData.setFee(fee); + + TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer); + + return deployAtTransaction; + } + + private MessageTransaction sendMessage(Repository repository, PrivateKeyAccount sender, byte[] data, String recipient) throws DataException { + long txTimestamp = System.currentTimeMillis(); + byte[] lastReference = sender.getLastReference(); + + if (lastReference == null) { + System.err.println(String.format("Qortal account %s has no last reference", sender.getAddress())); + System.exit(2); + } + + Long fee = null; + int version = 4; + int nonce = 0; + long amount = 0; + Long assetId = null; // because amount is zero + + BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null); + TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, data, false, false); + + MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData); + + fee = messageTransaction.calcRecommendedFee(); + messageTransactionData.setFee(fee); + + TransactionUtils.signAndMint(repository, messageTransactionData, sender); + + return messageTransaction; + } + +} diff --git a/src/test/java/org/qortal/test/at/GetPartialMessageTests.java b/src/test/java/org/qortal/test/at/GetPartialMessageTests.java new file mode 100644 index 00000000..4bc9d9ea --- /dev/null +++ b/src/test/java/org/qortal/test/at/GetPartialMessageTests.java @@ -0,0 +1,221 @@ +package org.qortal.test.at; + +import static org.junit.Assert.*; + +import java.nio.ByteBuffer; + +import org.ciyam.at.CompilationException; +import org.ciyam.at.FunctionCode; +import org.ciyam.at.MachineState; +import org.ciyam.at.OpCode; +import org.junit.Before; +import org.junit.Test; +import org.qortal.account.PrivateKeyAccount; +import org.qortal.asset.Asset; +import org.qortal.at.QortalAtLoggerFactory; +import org.qortal.at.QortalFunctionCode; +import org.qortal.data.at.ATStateData; +import org.qortal.data.transaction.BaseTransactionData; +import org.qortal.data.transaction.DeployAtTransactionData; +import org.qortal.data.transaction.MessageTransactionData; +import org.qortal.data.transaction.TransactionData; +import org.qortal.group.Group; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; +import org.qortal.test.common.BlockUtils; +import org.qortal.test.common.Common; +import org.qortal.test.common.TransactionUtils; +import org.qortal.transaction.DeployAtTransaction; +import org.qortal.transaction.MessageTransaction; + +public class GetPartialMessageTests extends Common { + + @Before + public void before() throws DataException { + Common.useDefaultSettings(); + } + + @Test + public void testGetPartialMessage() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount deployer = Common.getTestAccount(repository, "alice"); + + byte[] messageData = "The quick brown fox jumped over the lazy dog.".getBytes(); + int[] offsets = new int[] { 0, 7, 32, 44, messageData.length }; + + byte[] creationBytes = buildGetPartialMessageAT(offsets); + + long fundingAmount = 1_00000000L; + DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount); + String atAddress = deployAtTransaction.getATAccount().getAddress(); + + sendMessage(repository, deployer, messageData, atAddress); + + for (int offset : offsets) { + // Mint another block so AT can process message + BlockUtils.mintBlock(repository); + + byte[] expectedData = new byte[32]; + int byteCount = Math.min(32, messageData.length - offset); + System.arraycopy(messageData, offset, expectedData, 0, byteCount); + + // Check AT result + ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress); + byte[] stateData = atStateData.getStateData(); + + QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance(); + byte[] dataBytes = MachineState.extractDataBytes(loggerFactory, stateData); + + byte[] actualData = new byte[32]; + System.arraycopy(dataBytes, MachineState.VALUE_SIZE, actualData, 0, 32); + + assertArrayEquals(expectedData, actualData); + } + } + } + + private byte[] buildGetPartialMessageAT(int... offsets) { + // Labels for data segment addresses + int addrCounter = 0; + + final int addrCopyOfBIndex = addrCounter++; + + // 2nd position for easy extraction + final int addrCopyOfB = addrCounter; + addrCounter += 4; + + final int addrResult = addrCounter++; + final int addrLastTxTimestamp = addrCounter++; + final int addrOffset = addrCounter++; + + // Data segment + ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE); + + dataByteBuffer.putLong(addrCopyOfB); + + // Code labels + Integer labelCheckTx = null; + + ByteBuffer codeByteBuffer = ByteBuffer.allocate(512); + + // Two-pass version + for (int pass = 0; pass < 2; ++pass) { + codeByteBuffer.clear(); + + try { + /* Initialization */ + + // Use AT creation 'timestamp' as starting point for finding transactions sent to AT + codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrLastTxTimestamp)); + + // Set restart position to after this opcode + codeByteBuffer.put(OpCode.SET_PCS.compile()); + + /* Loop, waiting for message to AT */ + + // Find next transaction to this AT since the last one (if any) + codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxTimestamp)); + // If no transaction found, A will be zero. If A is zero, set addrComparator to 1, otherwise 0. + codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.CHECK_A_IS_ZERO, addrResult)); + // If addrResult is zero (i.e. A is non-zero, transaction was found) then go check transaction + codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, OpCode.calcOffset(codeByteBuffer, labelCheckTx))); + // Stop and wait for next block + codeByteBuffer.put(OpCode.STP_IMD.compile()); + + /* Check transaction */ + labelCheckTx = codeByteBuffer.position(); + + // Generate code per offset + for (int i = 0; i < offsets.length; ++i) { + if (i > 0) + // Wait for next block + codeByteBuffer.put(OpCode.SLP_IMD.compile()); + + // Set offset + codeByteBuffer.put(OpCode.SET_VAL.compile(addrOffset, offsets[i])); + + // Extract partial message + codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrOffset)); + + // Copy B to data segment + codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrCopyOfBIndex)); + } + + // We're done + codeByteBuffer.put(OpCode.FIN_IMD.compile()); + } catch (CompilationException e) { + throw new IllegalStateException("Unable to compile AT?", e); + } + } + + codeByteBuffer.flip(); + + byte[] codeBytes = new byte[codeByteBuffer.limit()]; + codeByteBuffer.get(codeBytes); + + final short ciyamAtVersion = 2; + final short numCallStackPages = 0; + final short numUserStackPages = 0; + final long minActivationAmount = 0L; + + return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount); + } + + private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, byte[] creationBytes, long fundingAmount) throws DataException { + long txTimestamp = System.currentTimeMillis(); + byte[] lastReference = deployer.getLastReference(); + + if (lastReference == null) { + System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress())); + System.exit(2); + } + + Long fee = null; + String name = "Test AT"; + String description = "Test AT"; + String atType = "Test"; + String tags = "TEST"; + + BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null); + TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT); + + DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData); + + fee = deployAtTransaction.calcRecommendedFee(); + deployAtTransactionData.setFee(fee); + + TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer); + + return deployAtTransaction; + } + + private MessageTransaction sendMessage(Repository repository, PrivateKeyAccount sender, byte[] data, String recipient) throws DataException { + long txTimestamp = System.currentTimeMillis(); + byte[] lastReference = sender.getLastReference(); + + if (lastReference == null) { + System.err.println(String.format("Qortal account %s has no last reference", sender.getAddress())); + System.exit(2); + } + + Long fee = null; + int version = 4; + int nonce = 0; + long amount = 0; + Long assetId = null; // because amount is zero + + BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null); + TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, data, false, false); + + MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData); + + fee = messageTransaction.calcRecommendedFee(); + messageTransactionData.setFee(fee); + + TransactionUtils.signAndMint(repository, messageTransactionData, sender); + + return messageTransaction; + } + +} diff --git a/src/test/java/org/qortal/test/btcacct/ElectrumXTests.java b/src/test/java/org/qortal/test/btcacct/ElectrumXTests.java index a8c3cb12..3a958c79 100644 --- a/src/test/java/org/qortal/test/btcacct/ElectrumXTests.java +++ b/src/test/java/org/qortal/test/btcacct/ElectrumXTests.java @@ -61,7 +61,7 @@ public class ElectrumXTests { // Timestamp(int) is at 4 + 32 + 32 = 68 bytes offset int offset = 4 + 32 + 32; - int timestamp = BitTwiddling.fromLEBytes(blockHeader, offset); + int timestamp = BitTwiddling.intFromLEBytes(blockHeader, offset); System.out.println(String.format("Block %d timestamp: %d", height + i, timestamp)); } }