From 65ccb80aa4786ff2b9181aec3aaeca6d2bd2e6c5 Mon Sep 17 00:00:00 2001 From: catbref Date: Wed, 10 Jun 2020 09:10:24 +0100 Subject: [PATCH] AT-related changes: new Qortal functions, tests, etc. Added GET_MESSAGE_LENGTH_FROM_TX_IN_A and PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B. Replaced AT-1.3.4 with version including bug-fix for off-by-one data address bounds checking. Moved long-from-bytes method to BitTwiddling class. Renamed some methods to make it more obvious they work with little/big endian data. --- lib/org/ciyam/AT/1.3.4/AT-1.3.4.jar | Bin 146000 -> 146003 bytes lib/org/ciyam/AT/maven-metadata-local.xml | 2 +- src/main/java/org/qortal/at/QortalATAPI.java | 56 ++--- .../org/qortal/at/QortalFunctionCode.java | 71 +++++- src/main/java/org/qortal/crosschain/BTC.java | 2 +- .../java/org/qortal/utils/BitTwiddling.java | 8 +- .../qortal/test/at/GetMessageLengthTests.java | 223 ++++++++++++++++++ .../test/at/GetPartialMessageTests.java | 221 +++++++++++++++++ .../qortal/test/btcacct/ElectrumXTests.java | 2 +- 9 files changed, 549 insertions(+), 36 deletions(-) create mode 100644 src/test/java/org/qortal/test/at/GetMessageLengthTests.java create mode 100644 src/test/java/org/qortal/test/at/GetPartialMessageTests.java 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 5abe2c77a5b209f52f7ce11f7cdc97e29f871890..611836faf3ae0de20dd5f4c33cf19e2bb73c269c 100644 GIT binary patch delta 5743 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 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)); } }