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