forked from Qortal/qortal
Merge branch 'chat' into launch
This commit is contained in:
commit
edb56b74da
@ -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"),
|
||||
|
145
src/main/java/org/qortal/api/resource/ChatResource.java
Normal file
145
src/main/java/org/qortal/api/resource/ChatResource.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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)
|
||||
|
17
src/main/java/org/qortal/repository/ChatRepository.java
Normal file
17
src/main/java/org/qortal/repository/ChatRepository.java
Normal 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;
|
||||
|
||||
}
|
@ -12,6 +12,8 @@ public interface Repository extends AutoCloseable {
|
||||
|
||||
public BlockRepository getBlockRepository();
|
||||
|
||||
public ChatRepository getChatRepository();
|
||||
|
||||
public GroupRepository getGroupRepository();
|
||||
|
||||
public NameRepository getNameRepository();
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
149
src/main/java/org/qortal/transaction/ChatTransaction.java
Normal file
149
src/main/java/org/qortal/transaction/ChatTransaction.java
Normal 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");
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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:
|
||||
|
48
src/test/java/org/qortal/test/apps/MemoryPoWTest.java
Normal file
48
src/test/java/org/qortal/test/apps/MemoryPoWTest.java
Normal 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));
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user