mirror of
https://github.com/Qortal/qortal.git
synced 2025-02-11 17:55:50 +00:00
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>
|
||||
<version>1.3.4</version>
|
||||
</versions>
|
||||
<lastUpdated>20200414162728</lastUpdated>
|
||||
<lastUpdated>20200609101009</lastUpdated>
|
||||
</versioning>
|
||||
</metadata>
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 {
|
||||
/**
|
||||
* <tt>0x0510</tt><br>
|
||||
* 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.<br>
|
||||
* <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) {
|
||||
@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>
|
||||
* <tt>0x0511</tt><br>
|
||||
* P2SH stored in lower 25 bytes of B.
|
||||
*/
|
||||
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>
|
||||
* <tt>0x0512</tt><br>
|
||||
* Qortal address stored in lower 25 bytes of B.
|
||||
*/
|
||||
CONVERT_B_TO_QORTAL(0x0512, 0, false) {
|
||||
|
@ -99,7 +99,7 @@ public class BTC {
|
||||
if (blockHeaders == null || blockHeaders.size() < 11)
|
||||
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...
|
||||
blockTimestamps.sort((a, b) -> Integer.compare(b, a));
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
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
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user