forked from Qortal/qortal
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.
This commit is contained in:
parent
cc13d1d0f1
commit
65ccb80aa4
Binary file not shown.
@ -7,6 +7,6 @@
|
|||||||
<versions>
|
<versions>
|
||||||
<version>1.3.4</version>
|
<version>1.3.4</version>
|
||||||
</versions>
|
</versions>
|
||||||
<lastUpdated>20200414162728</lastUpdated>
|
<lastUpdated>20200609101009</lastUpdated>
|
||||||
</versioning>
|
</versioning>
|
||||||
</metadata>
|
</metadata>
|
||||||
|
@ -37,6 +37,7 @@ import org.qortal.transaction.AtTransaction;
|
|||||||
import org.qortal.transaction.Transaction;
|
import org.qortal.transaction.Transaction;
|
||||||
import org.qortal.transaction.Transaction.TransactionType;
|
import org.qortal.transaction.Transaction.TransactionType;
|
||||||
import org.qortal.utils.Base58;
|
import org.qortal.utils.Base58;
|
||||||
|
import org.qortal.utils.BitTwiddling;
|
||||||
|
|
||||||
import com.google.common.primitives.Bytes;
|
import com.google.common.primitives.Bytes;
|
||||||
|
|
||||||
@ -133,9 +134,9 @@ public class QortalATAPI extends API {
|
|||||||
|
|
||||||
byte[] signature = blockSummaries.get(0).getSignature();
|
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.
|
// 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.setA2(state, BitTwiddling.longFromBEBytes(signature, 52));
|
||||||
this.setA3(state, fromBytes(signature, 60));
|
this.setA3(state, BitTwiddling.longFromBEBytes(signature, 60));
|
||||||
this.setA4(state, fromBytes(signature, 68));
|
this.setA4(state, BitTwiddling.longFromBEBytes(signature, 68));
|
||||||
} catch (DataException e) {
|
} catch (DataException e) {
|
||||||
throw new RuntimeException("AT API unable to fetch previous block?", 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
|
// 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();
|
byte[] signature = transaction.getTransactionData().getSignature();
|
||||||
this.setA2(state, fromBytes(signature, 8));
|
this.setA2(state, BitTwiddling.longFromBEBytes(signature, 8));
|
||||||
this.setA3(state, fromBytes(signature, 16));
|
this.setA3(state, BitTwiddling.longFromBEBytes(signature, 16));
|
||||||
this.setA4(state, fromBytes(signature, 24));
|
this.setA4(state, BitTwiddling.longFromBEBytes(signature, 24));
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -282,7 +283,7 @@ public class QortalATAPI extends API {
|
|||||||
|
|
||||||
byte[] hash = Crypto.digest(input);
|
byte[] hash = Crypto.digest(input);
|
||||||
|
|
||||||
return fromBytes(hash, 0);
|
return BitTwiddling.longFromBEBytes(hash, 0);
|
||||||
} catch (DataException e) {
|
} catch (DataException e) {
|
||||||
throw new RuntimeException("AT API unable to fetch latest block from repository?", 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);
|
TransactionData transactionData = this.getTransactionFromA(state);
|
||||||
|
|
||||||
byte[] messageData = null;
|
byte[] messageData = this.getMessageFromTransaction(transactionData);
|
||||||
|
|
||||||
switch (transactionData.getType()) {
|
|
||||||
case MESSAGE:
|
|
||||||
messageData = ((MessageTransactionData) transactionData).getData();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case AT:
|
|
||||||
messageData = ((ATTransactionData) transactionData).getMessage();
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check data length is appropriate, i.e. not larger than B
|
// Check data length is appropriate, i.e. not larger than B
|
||||||
if (messageData.length > 4 * 8)
|
if (messageData.length > 4 * 8)
|
||||||
@ -457,12 +445,6 @@ public class QortalATAPI extends API {
|
|||||||
|
|
||||||
// Utility methods
|
// 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. */
|
/** 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) {
|
public static byte[] partialSignature(byte[] fullSignature) {
|
||||||
return Arrays.copyOfRange(fullSignature, 8, 32);
|
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
|
// Compare end of transaction's signature against A2 thru A4
|
||||||
byte[] sig = transactionData.getSignature();
|
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");
|
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 */
|
/** Returns AT's account */
|
||||||
/* package */ Account getATAccount() {
|
/* package */ Account getATAccount() {
|
||||||
return new Account(this.repository, this.atData.getATAddress());
|
return new Account(this.repository, this.atData.getATAddress());
|
||||||
@ -563,4 +559,8 @@ public class QortalATAPI extends API {
|
|||||||
super.setB(state, bBytes);
|
super.setB(state, bBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void zeroB(MachineState state) {
|
||||||
|
super.zeroB(state);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import org.ciyam.at.IllegalFunctionCodeException;
|
|||||||
import org.ciyam.at.MachineState;
|
import org.ciyam.at.MachineState;
|
||||||
import org.qortal.crosschain.BTC;
|
import org.qortal.crosschain.BTC;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
|
import org.qortal.data.transaction.TransactionData;
|
||||||
import org.qortal.settings.Settings;
|
import org.qortal.settings.Settings;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -22,8 +23,70 @@ import org.qortal.settings.Settings;
|
|||||||
*/
|
*/
|
||||||
public enum QortalFunctionCode {
|
public enum QortalFunctionCode {
|
||||||
/**
|
/**
|
||||||
* <tt>0x0510</tt><br>
|
* Returns length of message data from transaction in A.<br>
|
||||||
* Convert address in B to 20-byte value in LSB of B1, and all of B2 & B3.
|
* <tt>0x0501</tt><br>
|
||||||
|
* 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<br>
|
||||||
|
* <tt>0x0502 start-offset</tt><br>
|
||||||
|
* Copies up to 32 bytes of message data, starting at <tt>start-offset</tt> into B.<br>
|
||||||
|
* If transaction has no 'message', or <tt>start-offset</tt> out of bounds, then zero B<br>
|
||||||
|
* 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.<br>
|
||||||
|
* <tt>0x0510</tt>
|
||||||
*/
|
*/
|
||||||
CONVERT_B_TO_PKH(0x0510, 0, false) {
|
CONVERT_B_TO_PKH(0x0510, 0, false) {
|
||||||
@Override
|
@Override
|
||||||
@ -38,8 +101,8 @@ public enum QortalFunctionCode {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* <tt>0x0511</tt><br>
|
|
||||||
* Convert 20-byte value in LSB of B1, and all of B2 & B3 to P2SH.<br>
|
* Convert 20-byte value in LSB of B1, and all of B2 & B3 to P2SH.<br>
|
||||||
|
* <tt>0x0511</tt><br>
|
||||||
* P2SH stored in lower 25 bytes of B.
|
* P2SH stored in lower 25 bytes of B.
|
||||||
*/
|
*/
|
||||||
CONVERT_B_TO_P2SH(0x0511, 0, false) {
|
CONVERT_B_TO_P2SH(0x0511, 0, false) {
|
||||||
@ -51,8 +114,8 @@ public enum QortalFunctionCode {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* <tt>0x0512</tt><br>
|
|
||||||
* Convert 20-byte value in LSB of B1, and all of B2 & B3 to Qortal address.<br>
|
* Convert 20-byte value in LSB of B1, and all of B2 & B3 to Qortal address.<br>
|
||||||
|
* <tt>0x0512</tt><br>
|
||||||
* Qortal address stored in lower 25 bytes of B.
|
* Qortal address stored in lower 25 bytes of B.
|
||||||
*/
|
*/
|
||||||
CONVERT_B_TO_QORTAL(0x0512, 0, false) {
|
CONVERT_B_TO_QORTAL(0x0512, 0, false) {
|
||||||
|
@ -99,7 +99,7 @@ public class BTC {
|
|||||||
if (blockHeaders == null || blockHeaders.size() < 11)
|
if (blockHeaders == null || blockHeaders.size() < 11)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
List<Integer> blockTimestamps = blockHeaders.stream().map(blockHeader -> BitTwiddling.fromLEBytes(blockHeader, TIMESTAMP_OFFSET)).collect(Collectors.toList());
|
List<Integer> blockTimestamps = blockHeaders.stream().map(blockHeader -> BitTwiddling.intFromLEBytes(blockHeader, TIMESTAMP_OFFSET)).collect(Collectors.toList());
|
||||||
|
|
||||||
// Descending, but order shouldn't matter as we're picking median...
|
// Descending, but order shouldn't matter as we're picking median...
|
||||||
blockTimestamps.sort((a, b) -> Integer.compare(b, a));
|
blockTimestamps.sort((a, b) -> Integer.compare(b, a));
|
||||||
|
@ -27,8 +27,14 @@ public class BitTwiddling {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Convert little-endian bytes to int */
|
/** 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;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
223
src/test/java/org/qortal/test/at/GetMessageLengthTests.java
Normal file
223
src/test/java/org/qortal/test/at/GetMessageLengthTests.java
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
221
src/test/java/org/qortal/test/at/GetPartialMessageTests.java
Normal file
221
src/test/java/org/qortal/test/at/GetPartialMessageTests.java
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -61,7 +61,7 @@ public class ElectrumXTests {
|
|||||||
|
|
||||||
// Timestamp(int) is at 4 + 32 + 32 = 68 bytes offset
|
// Timestamp(int) is at 4 + 32 + 32 = 68 bytes offset
|
||||||
int offset = 4 + 32 + 32;
|
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));
|
System.out.println(String.format("Block %d timestamp: %d", height + i, timestamp));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user