Merge branch 'chat' into launch

This commit is contained in:
catbref 2020-05-01 10:51:38 +01:00
commit edb56b74da
17 changed files with 934 additions and 18 deletions

View File

@ -15,6 +15,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
@Tag(name = "Assets"),
@Tag(name = "Automated Transactions"),
@Tag(name = "Blocks"),
@Tag(name = "Chat"),
@Tag(name = "Cross-Chain"),
@Tag(name = "Groups"),
@Tag(name = "Names"),

View File

@ -0,0 +1,145 @@
package org.qortal.api.resource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import org.qortal.api.ApiError;
import org.qortal.api.ApiErrors;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.crypto.Crypto;
import org.qortal.data.transaction.ChatTransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.transaction.ChatTransaction;
import org.qortal.transaction.Transaction;
import org.qortal.transaction.Transaction.ValidationResult;
import org.qortal.transform.TransformationException;
import org.qortal.transform.transaction.ChatTransactionTransformer;
import org.qortal.utils.Base58;
@Path("/chat")
@Tag(name = "Chat")
public class ChatResource {
@Context
HttpServletRequest request;
@GET
@Path("/search")
@Operation(
summary = "Find chat messages",
description = "Returns CHAT transactions that match criteria.",
responses = {
@ApiResponse(
description = "transactions",
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = ChatTransactionData.class
)
)
)
)
}
)
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
public List<ChatTransactionData> searchChat(@QueryParam("before") Long before, @QueryParam("after") Long after,
@QueryParam("txGroupId") Integer txGroupId,
@QueryParam("sender") String senderAddress,
@QueryParam("recipient") String recipientAddress,
@Parameter(ref = "limit") @QueryParam("limit") Integer limit,
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
@Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) {
// Check any provided addresses are valid
if (senderAddress != null && !Crypto.isValidAddress(senderAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
if (recipientAddress != null && !Crypto.isValidAddress(recipientAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
if (before != null && before < 1500000000000L)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
if (after != null && after < 1500000000000L)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getChatRepository().getTransactionsMatchingCriteria(
before,
after,
txGroupId,
senderAddress,
recipientAddress,
limit, offset, reverse);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@POST
@Operation(
summary = "Build raw, unsigned, CHAT transaction",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = ChatTransactionData.class
)
)
),
responses = {
@ApiResponse(
description = "raw, unsigned, CHAT transaction encoded in Base58",
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string"
)
)
)
}
)
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
public String buildChat(ChatTransactionData transactionData) {
try (final Repository repository = RepositoryManager.getRepository()) {
ChatTransaction chatTransaction = (ChatTransaction) Transaction.fromData(repository, transactionData);
// Quicker validity check first before we compute nonce
ValidationResult result = chatTransaction.isValid();
if (result != ValidationResult.OK)
throw TransactionsResource.createTransactionInvalidException(request, result);
chatTransaction.computeNonce();
result = chatTransaction.isValidUnconfirmed();
if (result != ValidationResult.OK)
throw TransactionsResource.createTransactionInvalidException(request, result);
byte[] bytes = ChatTransactionTransformer.toBytes(transactionData);
return Base58.encode(bytes);
} catch (TransformationException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
}

View File

@ -1,29 +1,30 @@
package org.qortal.crypto;
import java.nio.ByteBuffer;
import com.google.common.primitives.Bytes;
public class MemoryPoW {
public static final int WORK_BUFFER_LENGTH = 4 * 1024 * 1024;
private static final int WORK_BUFFER_LENGTH_MASK = WORK_BUFFER_LENGTH - 1;
private static final int HASH_LENGTH = 32;
private static final int HASH_LENGTH_MASK = HASH_LENGTH - 1;
public static Integer compute(byte[] data, int start, int range, int difficulty) {
public static Integer compute(byte[] data, int workBufferLength, int start, int range, int difficulty) {
if (range < 1)
throw new IllegalArgumentException("range must be at least 1");
if (difficulty < 1)
throw new IllegalArgumentException("difficulty must be at least 1");
final int workBufferLengthMask = workBufferLength - 1;
// Hash data with SHA256
byte[] hash = Crypto.digest(data);
assert hash.length == HASH_LENGTH;
byte[] perturbedHash = new byte[HASH_LENGTH];
byte[] workBuffer = new byte[WORK_BUFFER_LENGTH];
byte[] workBuffer = new byte[workBufferLength];
byte[] bufferHash = new byte[HASH_LENGTH];
// For each nonce...
@ -41,7 +42,7 @@ public class MemoryPoW {
int hashOffset = 0;
for (int workBufferOffset = 0; workBufferOffset < WORK_BUFFER_LENGTH; workBufferOffset += HASH_LENGTH) {
for (int workBufferOffset = 0; workBufferOffset < workBufferLength; workBufferOffset += HASH_LENGTH) {
System.arraycopy(perturbedHash, 0, workBuffer, workBufferOffset, HASH_LENGTH);
hashOffset = ++hashOffset & HASH_LENGTH_MASK;
@ -55,7 +56,7 @@ public class MemoryPoW {
perturbedHash[hi] = (byte) (hashByte ^ (ch + hi));
}
workBuffer[wanderingBufferOffset & WORK_BUFFER_LENGTH_MASK] ^= 0xAA;
workBuffer[wanderingBufferOffset & workBufferLengthMask] ^= 0xAA;
// final int finalWanderingBufferOffset = wanderingBufferOffset & WORK_BUFFER_LENGTH_MASK;
// System.out.println(String.format("wanderingBufferOffset: 0x%08x / 0x%08x - %02d%%", finalWanderingBufferOffset, WORK_BUFFER_LENGTH, finalWanderingBufferOffset * 100 / WORK_BUFFER_LENGTH));
@ -80,4 +81,109 @@ public class MemoryPoW {
return null;
}
public static Integer compute2(byte[] data, int workBufferLength, long difficulty) {
// Hash data with SHA256
byte[] hash = Crypto.digest(data);
long[] longHash = new long[4];
ByteBuffer byteBuffer = ByteBuffer.wrap(hash);
longHash[0] = byteBuffer.getLong();
longHash[1] = byteBuffer.getLong();
longHash[2] = byteBuffer.getLong();
longHash[3] = byteBuffer.getLong();
byteBuffer = null;
int longBufferLength = workBufferLength / 8;
long[] workBuffer = new long[longBufferLength / 8];
long[] state = new long[4];
long seed = 8682522807148012L;
long seedMultiplier = 1181783497276652981L;
// For each nonce...
int nonce = -1;
long result = 0;
do {
++nonce;
seed *= seedMultiplier; // per nonce
state[0] = longHash[0] ^ seed;
state[1] = longHash[1] ^ seed;
state[2] = longHash[2] ^ seed;
state[3] = longHash[3] ^ seed;
// Fill work buffer with random
for (int i = 0; i < workBuffer.length; ++i)
workBuffer[i] = xoshiro256p(state);
// Random bounce through whole buffer
result = workBuffer[0];
for (int i = 0; i < 1024; ++i) {
int index = (int) (xoshiro256p(state) & Integer.MAX_VALUE) % workBuffer.length;
result ^= workBuffer[index];
}
// Return if final value > difficulty
} while (Long.numberOfLeadingZeros(result) < difficulty);
return nonce;
}
public static boolean verify2(byte[] data, int workBufferLength, long difficulty, int nonce) {
// Hash data with SHA256
byte[] hash = Crypto.digest(data);
long[] longHash = new long[4];
ByteBuffer byteBuffer = ByteBuffer.wrap(hash);
longHash[0] = byteBuffer.getLong();
longHash[1] = byteBuffer.getLong();
longHash[2] = byteBuffer.getLong();
longHash[3] = byteBuffer.getLong();
byteBuffer = null;
int longBufferLength = workBufferLength / 8;
long[] workBuffer = new long[longBufferLength / 8];
long[] state = new long[4];
long seed = 8682522807148012L;
long seedMultiplier = 1181783497276652981L;
for (int i = 0; i <= nonce; ++i)
seed *= seedMultiplier;
state[0] = longHash[0] ^ seed;
state[1] = longHash[1] ^ seed;
state[2] = longHash[2] ^ seed;
state[3] = longHash[3] ^ seed;
// Fill work buffer with random
for (int i = 0; i < workBuffer.length; ++i)
workBuffer[i] = xoshiro256p(state);
// Random bounce through whole buffer
long result = workBuffer[0];
for (int i = 0; i < 1024; ++i) {
int index = (int) (xoshiro256p(state) & Integer.MAX_VALUE) % workBuffer.length;
result ^= workBuffer[index];
}
return Long.numberOfLeadingZeros(result) >= difficulty;
}
private static final long xoshiro256p(long[] state) {
final long result = state[0] + state[3];
final long temp = state[1] << 17;
state[2] ^= state[0];
state[3] ^= state[1];
state[1] ^= state[2];
state[0] ^= state[3];
state[2] ^= temp;
state[3] = (state[3] << 45) | (state[3] >>> (64 - 45)); // rol64(s[3], 45);
return result;
}
}

View File

@ -0,0 +1,93 @@
package org.qortal.data.transaction;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import org.qortal.transaction.Transaction.TransactionType;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.media.Schema.AccessMode;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
@Schema(allOf = { TransactionData.class })
public class ChatTransactionData extends TransactionData {
// Properties
@Schema(description = "sender's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP")
private byte[] senderPublicKey;
@Schema(accessMode = AccessMode.READ_ONLY)
private String sender;
@Schema(accessMode = AccessMode.READ_ONLY)
private int nonce;
private String recipient; // can be null
@Schema(description = "raw message data, possibly UTF8 text", example = "2yGEbwRFyhPZZckKA")
private byte[] data;
private boolean isText;
private boolean isEncrypted;
// Constructors
// For JAXB
protected ChatTransactionData() {
super(TransactionType.CHAT);
}
public void afterUnmarshal(Unmarshaller u, Object parent) {
this.creatorPublicKey = this.senderPublicKey;
}
public ChatTransactionData(BaseTransactionData baseTransactionData,
String sender, int nonce, String recipient, byte[] data, boolean isText, boolean isEncrypted) {
super(TransactionType.CHAT, baseTransactionData);
this.senderPublicKey = baseTransactionData.creatorPublicKey;
this.sender = sender;
this.nonce = nonce;
this.recipient = recipient;
this.data = data;
this.isText = isText;
this.isEncrypted = isEncrypted;
}
// Getters/Setters
public byte[] getSenderPublicKey() {
return this.senderPublicKey;
}
public String getSender() {
return this.sender;
}
public int getNonce() {
return this.nonce;
}
public void setNonce(int nonce) {
this.nonce = nonce;
}
public String getRecipient() {
return this.recipient;
}
public byte[] getData() {
return this.data;
}
public boolean getIsText() {
return this.isText;
}
public boolean getIsEncrypted() {
return this.isEncrypted;
}
}

View File

@ -40,7 +40,7 @@ import io.swagger.v3.oas.annotations.media.Schema.AccessMode;
GroupApprovalTransactionData.class, SetGroupTransactionData.class,
UpdateAssetTransactionData.class,
AccountFlagsTransactionData.class, RewardShareTransactionData.class,
AccountLevelTransactionData.class
AccountLevelTransactionData.class, ChatTransactionData.class
})
//All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)

View File

@ -0,0 +1,17 @@
package org.qortal.repository;
import java.util.List;
import org.qortal.data.transaction.ChatTransactionData;
public interface ChatRepository {
public List<ChatTransactionData> getTransactionsMatchingCriteria(
Long before,
Long after,
Integer txGroupId,
String senderAddress,
String recipientAddress,
Integer limit, Integer offset, Boolean reverse) throws DataException;
}

View File

@ -12,6 +12,8 @@ public interface Repository extends AutoCloseable {
public BlockRepository getBlockRepository();
public ChatRepository getChatRepository();
public GroupRepository getGroupRepository();
public NameRepository getNameRepository();

View File

@ -0,0 +1,109 @@
package org.qortal.repository.hsqldb;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import org.qortal.data.transaction.ChatTransactionData;
import org.qortal.repository.ChatRepository;
import org.qortal.repository.DataException;
import org.qortal.transaction.Transaction.TransactionType;
public class HSQLDBChatRepository implements ChatRepository {
protected HSQLDBRepository repository;
public HSQLDBChatRepository(HSQLDBRepository repository) {
this.repository = repository;
}
@Override
public List<ChatTransactionData> getTransactionsMatchingCriteria(Long before, Long after, Integer txGroupId,
String senderAddress, String recipientAddress, Integer limit, Integer offset, Boolean reverse)
throws DataException {
boolean hasSenderAddress = senderAddress != null && !senderAddress.isEmpty();
boolean hasRecipientAddress = recipientAddress != null && !recipientAddress.isEmpty();
String signatureColumn = "Transactions.signature";
List<String> whereClauses = new ArrayList<>();
List<Object> bindParams = new ArrayList<>();
// Tables, starting with Transactions
StringBuilder tables = new StringBuilder(256);
tables.append("Transactions");
if (hasSenderAddress || hasRecipientAddress)
tables.append(" JOIN ChatTransactions USING (signature)");
// WHERE clauses next
// CHAT transaction type
whereClauses.add("Transactions.type = " + TransactionType.CHAT.value);
// Timestamp range
if (before != null) {
whereClauses.add("Transactions.creation < ?");
bindParams.add(HSQLDBRepository.toOffsetDateTime(before));
}
if (after != null) {
whereClauses.add("Transactions.creation > ?");
bindParams.add(HSQLDBRepository.toOffsetDateTime(after));
}
if (txGroupId != null)
whereClauses.add("Transactions.tx_group_id = " + txGroupId);
if (hasSenderAddress) {
whereClauses.add("ChatTransactions.sender = ?");
bindParams.add(senderAddress);
}
if (hasRecipientAddress) {
whereClauses.add("ChatTransactions.recipient = ?");
bindParams.add(recipientAddress);
}
StringBuilder sql = new StringBuilder(1024);
sql.append("SELECT ");
sql.append(signatureColumn);
sql.append(" FROM ");
sql.append(tables);
if (!whereClauses.isEmpty()) {
sql.append(" WHERE ");
final int whereClausesSize = whereClauses.size();
for (int wci = 0; wci < whereClausesSize; ++wci) {
if (wci != 0)
sql.append(" AND ");
sql.append(whereClauses.get(wci));
}
}
sql.append(" ORDER BY Transactions.creation");
sql.append((reverse == null || !reverse) ? " ASC" : " DESC");
HSQLDBRepository.limitOffsetSql(sql, limit, offset);
List<ChatTransactionData> chatTransactionsData = new ArrayList<>();
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), bindParams.toArray())) {
if (resultSet == null)
return chatTransactionsData;
do {
byte[] signature = resultSet.getBytes(1);
chatTransactionsData.add((ChatTransactionData) this.repository.getTransactionRepository().fromSignature(signature));
} while (resultSet.next());
return chatTransactionsData;
} catch (SQLException e) {
throw new DataException("Unable to fetch matching chat transactions from repository", e);
}
}
}

View File

@ -986,6 +986,13 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("ALTER TABLE ATs ADD COLUMN code_hash VARBINARY(32) NOT NULL BEFORE is_sleeping"); // Assuming something like SHA256
break;
case 73:
// Chat transactions
stmt.execute("CREATE TABLE ChatTransactions (signature Signature, sender QortalAddress NOT NULL, nonce INT NOT NULL, recipient QortalAddress, "
+ "is_text BOOLEAN NOT NULL, is_encrypted BOOLEAN NOT NULL, data MessageData NOT NULL, "
+ "PRIMARY KEY (signature), FOREIGN KEY (signature) REFERENCES Transactions (signature) ON DELETE CASCADE)");
break;
default:
// nothing to do
return false;

View File

@ -35,6 +35,7 @@ import org.qortal.repository.AccountRepository;
import org.qortal.repository.ArbitraryRepository;
import org.qortal.repository.AssetRepository;
import org.qortal.repository.BlockRepository;
import org.qortal.repository.ChatRepository;
import org.qortal.repository.DataException;
import org.qortal.repository.GroupRepository;
import org.qortal.repository.NameRepository;
@ -115,6 +116,11 @@ public class HSQLDBRepository implements Repository {
return new HSQLDBBlockRepository(this);
}
@Override
public ChatRepository getChatRepository() {
return new HSQLDBChatRepository(this);
}
@Override
public GroupRepository getGroupRepository() {
return new HSQLDBGroupRepository(this);

View File

@ -0,0 +1,57 @@
package org.qortal.repository.hsqldb.transaction;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.ChatTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.hsqldb.HSQLDBRepository;
import org.qortal.repository.hsqldb.HSQLDBSaver;
public class HSQLDBChatTransactionRepository extends HSQLDBTransactionRepository {
public HSQLDBChatTransactionRepository(HSQLDBRepository repository) {
this.repository = repository;
}
TransactionData fromBase(BaseTransactionData baseTransactionData) throws DataException {
String sql = "SELECT sender, nonce, recipient, is_text, is_encrypted, data FROM ChatTransactions WHERE signature = ?";
try (ResultSet resultSet = this.repository.checkedExecute(sql, baseTransactionData.getSignature())) {
if (resultSet == null)
return null;
String sender = resultSet.getString(1);
int nonce = resultSet.getInt(2);
String recipient = resultSet.getString(3);
boolean isText = resultSet.getBoolean(4);
boolean isEncrypted = resultSet.getBoolean(5);
byte[] data = resultSet.getBytes(6);
return new ChatTransactionData(baseTransactionData, sender, nonce, recipient, data, isText, isEncrypted);
} catch (SQLException e) {
throw new DataException("Unable to fetch chat transaction from repository", e);
}
}
@Override
public void save(TransactionData transactionData) throws DataException {
ChatTransactionData chatTransactionData = (ChatTransactionData) transactionData;
HSQLDBSaver saveHelper = new HSQLDBSaver("ChatTransactions");
saveHelper.bind("signature", chatTransactionData.getSignature()).bind("nonce", chatTransactionData.getNonce())
.bind("sender", chatTransactionData.getSender()).bind("recipient", chatTransactionData.getRecipient())
.bind("is_text", chatTransactionData.getIsText()).bind("is_encrypted", chatTransactionData.getIsEncrypted())
.bind("data", chatTransactionData.getData());
try {
saveHelper.execute(this.repository);
} catch (SQLException e) {
throw new DataException("Unable to save chat transaction into repository", e);
}
}
}

View File

@ -0,0 +1,149 @@
package org.qortal.transaction;
import java.util.Collections;
import java.util.List;
import org.qortal.account.Account;
import org.qortal.account.PublicKeyAccount;
import org.qortal.crypto.Crypto;
import org.qortal.crypto.MemoryPoW;
import org.qortal.data.transaction.ChatTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.transform.TransformationException;
import org.qortal.transform.transaction.ChatTransactionTransformer;
import org.qortal.transform.transaction.TransactionTransformer;
public class ChatTransaction extends Transaction {
// Properties
private ChatTransactionData chatTransactionData;
// Other useful constants
public static final int MAX_DATA_SIZE = 256;
public static final int POW_BUFFER_SIZE = 8 * 1024 * 1024; // bytes
public static final int POW_DIFFICULTY = 12; // leading zero bits
// Constructors
public ChatTransaction(Repository repository, TransactionData transactionData) {
super(repository, transactionData);
this.chatTransactionData = (ChatTransactionData) this.transactionData;
}
// More information
@Override
public List<String> getRecipientAddresses() throws DataException {
String recipientAddress = this.chatTransactionData.getRecipient();
if (recipientAddress == null)
return Collections.emptyList();
return Collections.singletonList(recipientAddress);
}
// Navigation
public Account getSender() {
return this.getCreator();
}
public Account getRecipient() {
String recipientAddress = chatTransactionData.getRecipient();
if (recipientAddress == null)
return null;
return new Account(this.repository, recipientAddress);
}
// Processing
public void computeNonce() {
byte[] transactionBytes;
try {
transactionBytes = TransactionTransformer.toBytesForSigning(this.transactionData);
} catch (TransformationException e) {
throw new RuntimeException("Unable to transform transaction to byte array for verification", e);
}
// Clear nonce from transactionBytes
ChatTransactionTransformer.clearNonce(transactionBytes);
// Calculate nonce
this.chatTransactionData.setNonce(MemoryPoW.compute2(transactionBytes, POW_BUFFER_SIZE, POW_DIFFICULTY));
}
@Override
public ValidationResult isFeeValid() throws DataException {
if (this.transactionData.getFee() < 0)
return ValidationResult.NEGATIVE_FEE;
return ValidationResult.OK;
}
@Override
public boolean hasValidReference() throws DataException {
return true;
}
@Override
public ValidationResult isValid() throws DataException {
// If we exist in the repository then we've been imported as unconfirmed,
// but we don't want to make it into a block, so return false.
if (this.repository.getTransactionRepository().exists(this.chatTransactionData.getSignature()))
return ValidationResult.CHAT;
// If we have a recipient, check it is a valid address
String recipientAddress = chatTransactionData.getRecipient();
if (recipientAddress != null && !Crypto.isValidAddress(recipientAddress))
return ValidationResult.INVALID_ADDRESS;
// Check data length
if (chatTransactionData.getData().length < 1 || chatTransactionData.getData().length > MAX_DATA_SIZE)
return ValidationResult.INVALID_DATA_LENGTH;
// Nonce checking is done via isSignatureValid() as that method is only called once per import
return ValidationResult.OK;
}
@Override
public boolean isSignatureValid() {
byte[] signature = this.transactionData.getSignature();
if (signature == null)
return false;
byte[] transactionBytes;
try {
transactionBytes = ChatTransactionTransformer.toBytesForSigning(this.transactionData);
} catch (TransformationException e) {
throw new RuntimeException("Unable to transform transaction to byte array for verification", e);
}
if (!PublicKeyAccount.verify(this.transactionData.getCreatorPublicKey(), signature, transactionBytes))
return false;
int nonce = this.chatTransactionData.getNonce();
// Clear nonce from transactionBytes
ChatTransactionTransformer.clearNonce(transactionBytes);
// Check nonce
return MemoryPoW.verify2(transactionBytes, POW_BUFFER_SIZE, POW_DIFFICULTY, nonce);
}
@Override
public void process() throws DataException {
throw new DataException("CHAT transactions should never be processed");
}
@Override
public void orphan() throws DataException {
throw new DataException("CHAT transactions should never be orphaned");
}
}

View File

@ -59,7 +59,7 @@ public abstract class Transaction {
MULTI_PAYMENT(15, false),
DEPLOY_AT(16, true),
MESSAGE(17, true),
DELEGATION(18, false),
CHAT(18, false),
SUPERNODE(19, false),
AIRDROP(20, false),
AT(21, false),
@ -240,6 +240,7 @@ public abstract class Transaction {
SELF_SHARE_EXISTS(91),
ACCOUNT_ALREADY_EXISTS(92),
INVALID_GROUP_BLOCK_DELAY(93),
CHAT(999),
NOT_YET_RELEASED(1000);
public final int value;

View File

@ -0,0 +1,145 @@
package org.qortal.transform.transaction;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import org.qortal.crypto.Crypto;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.ChatTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.transaction.MessageTransaction;
import org.qortal.transaction.Transaction.TransactionType;
import org.qortal.transform.TransformationException;
import org.qortal.utils.Serialization;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
public class ChatTransactionTransformer extends TransactionTransformer {
// Property lengths
private static final int NONCE_LENGTH = INT_LENGTH;
private static final int HAS_RECIPIENT_LENGTH = BOOLEAN_LENGTH;
private static final int RECIPIENT_LENGTH = ADDRESS_LENGTH;
private static final int DATA_SIZE_LENGTH = INT_LENGTH;
private static final int IS_TEXT_LENGTH = BOOLEAN_LENGTH;
private static final int IS_ENCRYPTED_LENGTH = BOOLEAN_LENGTH;
private static final int EXTRAS_LENGTH = NONCE_LENGTH + HAS_RECIPIENT_LENGTH + DATA_SIZE_LENGTH + IS_ENCRYPTED_LENGTH + IS_TEXT_LENGTH;
protected static final TransactionLayout layout;
static {
layout = new TransactionLayout();
layout.add("txType: " + TransactionType.CHAT.valueString, TransformationType.INT);
layout.add("timestamp", TransformationType.TIMESTAMP);
layout.add("transaction's groupID", TransformationType.INT);
layout.add("reference", TransformationType.SIGNATURE);
layout.add("sender's public key", TransformationType.PUBLIC_KEY);
layout.add("proof-of-work nonce", TransformationType.INT);
layout.add("has recipient?", TransformationType.BOOLEAN);
layout.add("? recipient", TransformationType.ADDRESS);
layout.add("message length", TransformationType.INT);
layout.add("message", TransformationType.DATA);
layout.add("is message encrypted?", TransformationType.BOOLEAN);
layout.add("is message text?", TransformationType.BOOLEAN);
layout.add("fee", TransformationType.AMOUNT);
layout.add("signature", TransformationType.SIGNATURE);
}
public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException {
long timestamp = byteBuffer.getLong();
int txGroupId = byteBuffer.getInt();
byte[] reference = new byte[REFERENCE_LENGTH];
byteBuffer.get(reference);
byte[] senderPublicKey = Serialization.deserializePublicKey(byteBuffer);
int nonce = byteBuffer.getInt();
boolean hasRecipient = byteBuffer.get() != 0;
String recipient = hasRecipient ? Serialization.deserializeAddress(byteBuffer) : null;
int dataSize = byteBuffer.getInt();
// Don't allow invalid dataSize here to avoid run-time issues
if (dataSize > MessageTransaction.MAX_DATA_SIZE)
throw new TransformationException("MessageTransaction data size too large");
byte[] data = new byte[dataSize];
byteBuffer.get(data);
boolean isEncrypted = byteBuffer.get() != 0;
boolean isText = byteBuffer.get() != 0;
long fee = byteBuffer.getLong();
byte[] signature = new byte[SIGNATURE_LENGTH];
byteBuffer.get(signature);
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, senderPublicKey, fee, signature);
String sender = Crypto.toAddress(senderPublicKey);
return new ChatTransactionData(baseTransactionData, sender, nonce, recipient, data, isText, isEncrypted);
}
public static int getDataLength(TransactionData transactionData) throws TransformationException {
ChatTransactionData chatTransactionData = (ChatTransactionData) transactionData;
int dataLength = getBaseLength(transactionData) + EXTRAS_LENGTH + chatTransactionData.getData().length;
if (chatTransactionData.getRecipient() != null)
dataLength += RECIPIENT_LENGTH;
return dataLength;
}
public static byte[] toBytes(TransactionData transactionData) throws TransformationException {
try {
ChatTransactionData chatTransactionData = (ChatTransactionData) transactionData;
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
transformCommonBytes(transactionData, bytes);
bytes.write(Ints.toByteArray(chatTransactionData.getNonce()));
if (chatTransactionData.getRecipient() != null) {
bytes.write((byte) 1);
Serialization.serializeAddress(bytes, chatTransactionData.getRecipient());
} else {
bytes.write((byte) 0);
}
bytes.write(Ints.toByteArray(chatTransactionData.getData().length));
bytes.write(chatTransactionData.getData());
bytes.write((byte) (chatTransactionData.getIsEncrypted() ? 1 : 0));
bytes.write((byte) (chatTransactionData.getIsText() ? 1 : 0));
bytes.write(Longs.toByteArray(chatTransactionData.getFee()));
if (chatTransactionData.getSignature() != null)
bytes.write(chatTransactionData.getSignature());
return bytes.toByteArray();
} catch (IOException | ClassCastException e) {
throw new TransformationException(e);
}
}
public static void clearNonce(byte[] transactionBytes) {
int nonceIndex = TYPE_LENGTH + TIMESTAMP_LENGTH + GROUPID_LENGTH + REFERENCE_LENGTH + PUBLIC_KEY_LENGTH;
transactionBytes[nonceIndex++] = (byte) 0;
transactionBytes[nonceIndex++] = (byte) 0;
transactionBytes[nonceIndex++] = (byte) 0;
transactionBytes[nonceIndex++] = (byte) 0;
}
}

View File

@ -9,6 +9,11 @@ import java.util.Random;
public class MemoryPoWTests {
private static final int workBufferLength = 8 * 1024 * 1024;
private static final int start = 0;
private static final int range = 1000000;
private static final int difficulty = 11;
@Test
public void testCompute() {
Random random = new Random();
@ -16,15 +21,14 @@ public class MemoryPoWTests {
byte[] data = new byte[256];
random.nextBytes(data);
int start = 0;
int range = 1000000;
int difficulty = 1;
long startTime = System.currentTimeMillis();
Integer nonce = MemoryPoW.compute(data, start, range, difficulty);
// Integer nonce = MemoryPoW.compute(data, workBufferLength, start, range, difficulty);
int nonce = MemoryPoW.compute2(data, workBufferLength, difficulty);
long finishTime = System.currentTimeMillis();
System.out.println(String.format("Memory-hard PoW (buffer size: %dKB, range: %d, leading zeros: %d) took %dms", MemoryPoW.WORK_BUFFER_LENGTH / 1024, range, difficulty, finishTime - startTime));
System.out.println(String.format("Memory-hard PoW (buffer size: %dKB, range: %d, leading zeros: %d) took %dms", workBufferLength / 1024, range, difficulty, finishTime - startTime));
assertNotNull(nonce);
@ -33,8 +37,34 @@ public class MemoryPoWTests {
@Test
public void testMultipleComputes() {
for (int i = 0; i < 10; ++i)
testCompute();
Random random = new Random();
byte[] data = new byte[256];
int[] times = new int[100];
int timesS1 = 0;
int timesS2 = 0;
int maxNonce = 0;
for (int i = 0; i < times.length; ++i) {
random.nextBytes(data);
long startTime = System.currentTimeMillis();
int nonce = MemoryPoW.compute2(data, workBufferLength, difficulty);
times[i] = (int) (System.currentTimeMillis() - startTime);
timesS1 += times[i];
timesS2 += (times[i] * times[i]);
if (nonce > maxNonce)
maxNonce = nonce;
}
double stddev = Math.sqrt( ((double) times.length * timesS2 - timesS1 * timesS1) / ((double) times.length * (times.length - 1)) );
System.out.println(String.format("%d timings, mean: %d ms, stddev: %.2f ms", times.length, timesS1 / times.length, stddev));
System.out.println(String.format("Max nonce: %d", maxNonce));
}
}

View File

@ -47,7 +47,7 @@ public class SerializationTests extends Common {
case GENESIS:
case ACCOUNT_FLAGS:
case AT:
case DELEGATION:
case CHAT:
case SUPERNODE:
case AIRDROP:
case ENABLE_FORGING:

View File

@ -0,0 +1,48 @@
package org.qortal.test.apps;
import java.util.Random;
import org.qortal.crypto.MemoryPoW;
public class MemoryPoWTest {
public static void main(String[] args) {
if (args.length != 2) {
System.err.println("usage: MemoryPoW <buffer-size-MB> <difficulty>");
System.exit(2);
}
int workBufferLength = Integer.parseInt(args[0]) * 1024 * 1024;
int difficulty = Integer.parseInt(args[1]);
Random random = new Random();
byte[] data = new byte[256];
int[] times = new int[100];
int timesS1 = 0;
int timesS2 = 0;
int maxNonce = 0;
for (int i = 0; i < times.length; ++i) {
random.nextBytes(data);
long startTime = System.currentTimeMillis();
int nonce = MemoryPoW.compute2(data, workBufferLength, difficulty);
times[i] = (int) (System.currentTimeMillis() - startTime);
timesS1 += times[i];
timesS2 += (times[i] * times[i]);
if (nonce > maxNonce)
maxNonce = nonce;
}
double stddev = Math.sqrt( ((double) times.length * timesS2 - timesS1 * timesS1) / ((double) times.length * (times.length - 1)) );
System.out.println(String.format("%d timings, mean: %d ms, stddev: %.2f ms", times.length, timesS1 / times.length, stddev));
System.out.println(String.format("Max nonce: %d", maxNonce));
}
}