forked from Qortal/qortal
Merge branch 'PUBLICIZE-txn' into launch
This commit is contained in:
commit
9aabf93523
@ -28,6 +28,7 @@ import org.qortal.api.ApiError;
|
|||||||
import org.qortal.api.ApiErrors;
|
import org.qortal.api.ApiErrors;
|
||||||
import org.qortal.api.ApiException;
|
import org.qortal.api.ApiException;
|
||||||
import org.qortal.api.ApiExceptionFactory;
|
import org.qortal.api.ApiExceptionFactory;
|
||||||
|
import org.qortal.api.Security;
|
||||||
import org.qortal.api.model.ApiOnlineAccount;
|
import org.qortal.api.model.ApiOnlineAccount;
|
||||||
import org.qortal.api.model.RewardShareKeyRequest;
|
import org.qortal.api.model.RewardShareKeyRequest;
|
||||||
import org.qortal.asset.Asset;
|
import org.qortal.asset.Asset;
|
||||||
@ -36,19 +37,27 @@ import org.qortal.crypto.Crypto;
|
|||||||
import org.qortal.data.account.AccountData;
|
import org.qortal.data.account.AccountData;
|
||||||
import org.qortal.data.account.RewardShareData;
|
import org.qortal.data.account.RewardShareData;
|
||||||
import org.qortal.data.network.OnlineAccountData;
|
import org.qortal.data.network.OnlineAccountData;
|
||||||
|
import org.qortal.data.transaction.PublicizeTransactionData;
|
||||||
import org.qortal.data.transaction.RewardShareTransactionData;
|
import org.qortal.data.transaction.RewardShareTransactionData;
|
||||||
|
import org.qortal.data.transaction.TransactionData;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
import org.qortal.repository.Repository;
|
import org.qortal.repository.Repository;
|
||||||
import org.qortal.repository.RepositoryManager;
|
import org.qortal.repository.RepositoryManager;
|
||||||
import org.qortal.settings.Settings;
|
import org.qortal.settings.Settings;
|
||||||
|
import org.qortal.transaction.PublicizeTransaction;
|
||||||
import org.qortal.transaction.Transaction;
|
import org.qortal.transaction.Transaction;
|
||||||
|
import org.qortal.transaction.Transaction.TransactionType;
|
||||||
import org.qortal.transaction.Transaction.ValidationResult;
|
import org.qortal.transaction.Transaction.ValidationResult;
|
||||||
import org.qortal.transform.TransformationException;
|
import org.qortal.transform.TransformationException;
|
||||||
import org.qortal.transform.Transformer;
|
import org.qortal.transform.Transformer;
|
||||||
|
import org.qortal.transform.transaction.PublicizeTransactionTransformer;
|
||||||
import org.qortal.transform.transaction.RewardShareTransactionTransformer;
|
import org.qortal.transform.transaction.RewardShareTransactionTransformer;
|
||||||
|
import org.qortal.transform.transaction.TransactionTransformer;
|
||||||
import org.qortal.utils.Amounts;
|
import org.qortal.utils.Amounts;
|
||||||
import org.qortal.utils.Base58;
|
import org.qortal.utils.Base58;
|
||||||
|
|
||||||
|
import com.google.common.primitives.Bytes;
|
||||||
|
|
||||||
@Path("/addresses")
|
@Path("/addresses")
|
||||||
@Tag(name = "Addresses")
|
@Tag(name = "Addresses")
|
||||||
public class AddressesResource {
|
public class AddressesResource {
|
||||||
@ -390,4 +399,119 @@ public class AddressesResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/publicize")
|
||||||
|
@Operation(
|
||||||
|
summary = "Build raw, unsigned, PUBLICIZE transaction",
|
||||||
|
requestBody = @RequestBody(
|
||||||
|
required = true,
|
||||||
|
content = @Content(
|
||||||
|
mediaType = MediaType.APPLICATION_JSON,
|
||||||
|
schema = @Schema(
|
||||||
|
implementation = PublicizeTransactionData.class
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
description = "raw, unsigned, PUBLICIZE transaction encoded in Base58",
|
||||||
|
content = @Content(
|
||||||
|
mediaType = MediaType.TEXT_PLAIN,
|
||||||
|
schema = @Schema(
|
||||||
|
type = "string"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
|
||||||
|
public String publicize(PublicizeTransactionData transactionData) {
|
||||||
|
if (Settings.getInstance().isApiRestricted())
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
|
||||||
|
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||||
|
|
||||||
|
ValidationResult result = transaction.isValidUnconfirmed();
|
||||||
|
if (result != ValidationResult.OK && result != ValidationResult.INCORRECT_NONCE)
|
||||||
|
throw TransactionsResource.createTransactionInvalidException(request, result);
|
||||||
|
|
||||||
|
byte[] bytes = PublicizeTransactionTransformer.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/publicize/compute")
|
||||||
|
@Operation(
|
||||||
|
summary = "Compute nonce for raw, unsigned PUBLICIZE transaction",
|
||||||
|
requestBody = @RequestBody(
|
||||||
|
required = true,
|
||||||
|
content = @Content(
|
||||||
|
mediaType = MediaType.TEXT_PLAIN,
|
||||||
|
schema = @Schema(
|
||||||
|
type = "string",
|
||||||
|
description = "raw, unsigned PUBLICIZE transaction in base58 encoding",
|
||||||
|
example = "raw transaction base58"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
description = "raw, unsigned, PUBLICIZE transaction encoded in Base58",
|
||||||
|
content = @Content(
|
||||||
|
mediaType = MediaType.TEXT_PLAIN,
|
||||||
|
schema = @Schema(
|
||||||
|
type = "string"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({ApiError.TRANSACTION_INVALID, ApiError.INVALID_DATA, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
|
||||||
|
public String computePublicize(String rawBytes58) {
|
||||||
|
Security.checkApiCallAllowed(request);
|
||||||
|
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
byte[] rawBytes = Base58.decode(rawBytes58);
|
||||||
|
// We're expecting unsigned transaction, so append empty signature prior to decoding
|
||||||
|
rawBytes = Bytes.concat(rawBytes, new byte[TransactionTransformer.SIGNATURE_LENGTH]);
|
||||||
|
|
||||||
|
TransactionData transactionData = TransactionTransformer.fromBytes(rawBytes);
|
||||||
|
if (transactionData == null)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||||
|
|
||||||
|
if (transactionData.getType() != TransactionType.PUBLICIZE)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||||
|
|
||||||
|
PublicizeTransaction publicizeTransaction = (PublicizeTransaction) Transaction.fromData(repository, transactionData);
|
||||||
|
|
||||||
|
// Quicker validity check first before we compute nonce
|
||||||
|
ValidationResult result = publicizeTransaction.isValid();
|
||||||
|
if (result != ValidationResult.OK && result != ValidationResult.INCORRECT_NONCE)
|
||||||
|
throw TransactionsResource.createTransactionInvalidException(request, result);
|
||||||
|
|
||||||
|
publicizeTransaction.computeNonce();
|
||||||
|
|
||||||
|
// Re-check, but ignores signature
|
||||||
|
result = publicizeTransaction.isValidUnconfirmed();
|
||||||
|
if (result != ValidationResult.OK)
|
||||||
|
throw TransactionsResource.createTransactionInvalidException(request, result);
|
||||||
|
|
||||||
|
// Strip zeroed signature
|
||||||
|
transactionData.setSignature(null);
|
||||||
|
|
||||||
|
byte[] bytes = PublicizeTransactionTransformer.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -363,6 +363,60 @@ public class TransactionsResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/creator/{publickey}")
|
||||||
|
@Operation(
|
||||||
|
summary = "Find matching transactions created by account with given public key",
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
description = "transactions",
|
||||||
|
content = @Content(
|
||||||
|
array = @ArraySchema(
|
||||||
|
schema = @Schema(
|
||||||
|
implementation = TransactionData.class
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({
|
||||||
|
ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE
|
||||||
|
})
|
||||||
|
public List<TransactionData> findCreatorsTransactions(@PathParam("publickey") String publicKey58,
|
||||||
|
@Parameter(
|
||||||
|
description = "whether to include confirmed, unconfirmed or both",
|
||||||
|
required = true
|
||||||
|
) @QueryParam("confirmationStatus") ConfirmationStatus confirmationStatus, @Parameter(
|
||||||
|
ref = "limit"
|
||||||
|
) @QueryParam("limit") Integer limit, @Parameter(
|
||||||
|
ref = "offset"
|
||||||
|
) @QueryParam("offset") Integer offset, @Parameter(
|
||||||
|
ref = "reverse"
|
||||||
|
) @QueryParam("reverse") Boolean reverse) {
|
||||||
|
// Decode public key
|
||||||
|
byte[] publicKey;
|
||||||
|
try {
|
||||||
|
publicKey = Base58.decode(publicKey58);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
List<byte[]> signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null,
|
||||||
|
publicKey, confirmationStatus, limit, offset, reverse);
|
||||||
|
|
||||||
|
// Expand signatures to transactions
|
||||||
|
List<TransactionData> transactions = new ArrayList<>(signatures.size());
|
||||||
|
for (byte[] signature : signatures)
|
||||||
|
transactions.add(repository.getTransactionRepository().fromSignature(signature));
|
||||||
|
|
||||||
|
return transactions;
|
||||||
|
} catch (DataException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("/sign")
|
@Path("/sign")
|
||||||
@Operation(
|
@Operation(
|
||||||
|
@ -0,0 +1,56 @@
|
|||||||
|
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 PublicizeTransactionData extends TransactionData {
|
||||||
|
|
||||||
|
// Properties
|
||||||
|
@Schema(description = "sender's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP")
|
||||||
|
private byte[] senderPublicKey;
|
||||||
|
|
||||||
|
@Schema(accessMode = AccessMode.READ_ONLY)
|
||||||
|
private int nonce;
|
||||||
|
|
||||||
|
// Constructors
|
||||||
|
|
||||||
|
// For JAXB
|
||||||
|
protected PublicizeTransactionData() {
|
||||||
|
super(TransactionType.PUBLICIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void afterUnmarshal(Unmarshaller u, Object parent) {
|
||||||
|
this.creatorPublicKey = this.senderPublicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PublicizeTransactionData(BaseTransactionData baseTransactionData, int nonce) {
|
||||||
|
super(TransactionType.PUBLICIZE, baseTransactionData);
|
||||||
|
|
||||||
|
this.senderPublicKey = baseTransactionData.creatorPublicKey;
|
||||||
|
this.nonce = nonce;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters/Setters
|
||||||
|
|
||||||
|
public byte[] getSenderPublicKey() {
|
||||||
|
return this.senderPublicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getNonce() {
|
||||||
|
return this.nonce;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNonce(int nonce) {
|
||||||
|
this.nonce = nonce;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -72,6 +72,24 @@ public interface TransactionRepository {
|
|||||||
List<TransactionType> txTypes, Integer service, String address,
|
List<TransactionType> txTypes, Integer service, String address,
|
||||||
ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse) throws DataException;
|
ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns signatures for transactions that match search criteria.
|
||||||
|
* <p>
|
||||||
|
* Simpler version that only checks accepts one (optional) transaction type,
|
||||||
|
* and one (optional) public key.
|
||||||
|
*
|
||||||
|
* @param txType
|
||||||
|
* @param publicKey
|
||||||
|
* @param confirmationStatus
|
||||||
|
* @param limit
|
||||||
|
* @param offset
|
||||||
|
* @param reverse
|
||||||
|
* @return
|
||||||
|
* @throws DataException
|
||||||
|
*/
|
||||||
|
public List<byte[]> getSignaturesMatchingCriteria(TransactionType txType, byte[] publicKey,
|
||||||
|
ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns signature for latest auto-update transaction.
|
* Returns signature for latest auto-update transaction.
|
||||||
* <p>
|
* <p>
|
||||||
|
@ -613,6 +613,11 @@ public class HSQLDBDatabaseUpdates {
|
|||||||
stmt.execute("CREATE INDEX ChatTransactionsRecipientIndex ON ChatTransactions (recipient, sender)");
|
stmt.execute("CREATE INDEX ChatTransactionsRecipientIndex ON ChatTransactions (recipient, sender)");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 19:
|
||||||
|
// PUBLICIZE transactions
|
||||||
|
stmt.execute("CREATE TABLE PublicizeTransactions (signature Signature, nonce INT NOT NULL, " + TRANSACTION_KEYS + ")");
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// nothing to do
|
// nothing to do
|
||||||
return false;
|
return false;
|
||||||
|
@ -0,0 +1,50 @@
|
|||||||
|
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.PublicizeTransactionData;
|
||||||
|
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 HSQLDBPublicizeTransactionRepository extends HSQLDBTransactionRepository {
|
||||||
|
|
||||||
|
public HSQLDBPublicizeTransactionRepository(HSQLDBRepository repository) {
|
||||||
|
this.repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
TransactionData fromBase(BaseTransactionData baseTransactionData) throws DataException {
|
||||||
|
String sql = "SELECT nonce FROM PublicizeTransactions WHERE signature = ?";
|
||||||
|
|
||||||
|
try (ResultSet resultSet = this.repository.checkedExecute(sql, baseTransactionData.getSignature())) {
|
||||||
|
if (resultSet == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
int nonce = resultSet.getInt(1);
|
||||||
|
|
||||||
|
return new PublicizeTransactionData(baseTransactionData, nonce);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new DataException("Unable to fetch publicize transaction from repository", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void save(TransactionData transactionData) throws DataException {
|
||||||
|
PublicizeTransactionData publicizeTransactionData = (PublicizeTransactionData) transactionData;
|
||||||
|
|
||||||
|
HSQLDBSaver saveHelper = new HSQLDBSaver("PublicizeTransactions");
|
||||||
|
|
||||||
|
saveHelper.bind("signature", publicizeTransactionData.getSignature())
|
||||||
|
.bind("nonce", publicizeTransactionData.getNonce());
|
||||||
|
|
||||||
|
try {
|
||||||
|
saveHelper.execute(this.repository);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new DataException("Unable to save publicize transaction into repository", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -499,7 +499,75 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
|
|||||||
|
|
||||||
HSQLDBRepository.limitOffsetSql(sql, limit, offset);
|
HSQLDBRepository.limitOffsetSql(sql, limit, offset);
|
||||||
|
|
||||||
LOGGER.trace(String.format("Transaction search SQL: %s", sql));
|
LOGGER.trace(() -> String.format("Transaction search SQL: %s", sql));
|
||||||
|
|
||||||
|
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), bindParams.toArray())) {
|
||||||
|
if (resultSet == null)
|
||||||
|
return signatures;
|
||||||
|
|
||||||
|
do {
|
||||||
|
byte[] signature = resultSet.getBytes(1);
|
||||||
|
|
||||||
|
signatures.add(signature);
|
||||||
|
} while (resultSet.next());
|
||||||
|
|
||||||
|
return signatures;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new DataException("Unable to fetch matching transaction signatures from repository", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<byte[]> getSignaturesMatchingCriteria(TransactionType txType, byte[] publicKey,
|
||||||
|
ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||||
|
List<byte[]> signatures = new ArrayList<>();
|
||||||
|
|
||||||
|
StringBuilder sql = new StringBuilder(1024);
|
||||||
|
sql.append("SELECT signature FROM Transactions ");
|
||||||
|
|
||||||
|
List<String> whereClauses = new ArrayList<>();
|
||||||
|
List<Object> bindParams = new ArrayList<>();
|
||||||
|
|
||||||
|
if (txType != null) {
|
||||||
|
whereClauses.add("type = ?");
|
||||||
|
bindParams.add(txType.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (publicKey != null) {
|
||||||
|
whereClauses.add("creator = ?");
|
||||||
|
bindParams.add(publicKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (confirmationStatus) {
|
||||||
|
case BOTH:
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CONFIRMED:
|
||||||
|
whereClauses.add("Transactions.block_height IS NOT NULL");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UNCONFIRMED:
|
||||||
|
whereClauses.add("Transactions.block_height IS NULL");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.created_when");
|
||||||
|
sql.append((reverse == null || !reverse) ? " ASC" : " DESC");
|
||||||
|
|
||||||
|
HSQLDBRepository.limitOffsetSql(sql, limit, offset);
|
||||||
|
|
||||||
|
LOGGER.trace(() -> String.format("Transaction search SQL: %s", sql));
|
||||||
|
|
||||||
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), bindParams.toArray())) {
|
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), bindParams.toArray())) {
|
||||||
if (resultSet == null)
|
if (resultSet == null)
|
||||||
|
130
src/main/java/org/qortal/transaction/PublicizeTransaction.java
Normal file
130
src/main/java/org/qortal/transaction/PublicizeTransaction.java
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
package org.qortal.transaction;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.qortal.account.Account;
|
||||||
|
import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
|
||||||
|
import org.qortal.crypto.MemoryPoW;
|
||||||
|
import org.qortal.data.transaction.PublicizeTransactionData;
|
||||||
|
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.PublicizeTransactionTransformer;
|
||||||
|
import org.qortal.transform.transaction.TransactionTransformer;
|
||||||
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
|
public class PublicizeTransaction extends Transaction {
|
||||||
|
|
||||||
|
// Properties
|
||||||
|
private PublicizeTransactionData publicizeTransactionData;
|
||||||
|
|
||||||
|
// Other useful constants
|
||||||
|
|
||||||
|
/** If time difference between transaction and now is greater than this then we don't verify proof-of-work. */
|
||||||
|
public static final long HISTORIC_THRESHOLD = 2 * 7 * 24 * 60 * 60 * 1000L;
|
||||||
|
public static final int POW_BUFFER_SIZE = 8 * 1024 * 1024; // bytes
|
||||||
|
public static final int POW_DIFFICULTY = 18; // leading zero bits
|
||||||
|
|
||||||
|
// Constructors
|
||||||
|
|
||||||
|
public PublicizeTransaction(Repository repository, TransactionData transactionData) {
|
||||||
|
super(repository, transactionData);
|
||||||
|
|
||||||
|
this.publicizeTransactionData = (PublicizeTransactionData) this.transactionData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// More information
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> getRecipientAddresses() throws DataException {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
|
||||||
|
public Account getSender() {
|
||||||
|
return this.getCreator();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
PublicizeTransactionTransformer.clearNonce(transactionBytes);
|
||||||
|
|
||||||
|
// Calculate nonce
|
||||||
|
this.publicizeTransactionData.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 ValidationResult isValid() throws DataException {
|
||||||
|
// There can be only one
|
||||||
|
List<byte[]> signatures = this.repository.getTransactionRepository().getSignaturesMatchingCriteria(
|
||||||
|
TransactionType.PUBLICIZE,
|
||||||
|
this.transactionData.getCreatorPublicKey(),
|
||||||
|
ConfirmationStatus.CONFIRMED,
|
||||||
|
1, null, null);
|
||||||
|
|
||||||
|
if (!signatures.isEmpty())
|
||||||
|
return ValidationResult.TRANSACTION_ALREADY_EXISTS;
|
||||||
|
|
||||||
|
// We only need to check recent transactions due to PoW verification overhead
|
||||||
|
if (NTP.getTime() - this.transactionData.getTimestamp() < HISTORIC_THRESHOLD)
|
||||||
|
if (!verifyNonce())
|
||||||
|
return ValidationResult.INCORRECT_NONCE;
|
||||||
|
|
||||||
|
return ValidationResult.OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean verifyNonce() {
|
||||||
|
byte[] transactionBytes;
|
||||||
|
|
||||||
|
try {
|
||||||
|
transactionBytes = TransactionTransformer.toBytesForSigning(this.transactionData);
|
||||||
|
} catch (TransformationException e) {
|
||||||
|
throw new RuntimeException("Unable to transform transaction to byte array for verification", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
int nonce = this.publicizeTransactionData.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 {
|
||||||
|
// Save this transaction
|
||||||
|
this.repository.getTransactionRepository().save(this.transactionData);
|
||||||
|
|
||||||
|
// Ensure public key & address are saved
|
||||||
|
this.getSender().ensureAccount();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void orphan() throws DataException {
|
||||||
|
/* Don't actually need to do anything */
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -62,7 +62,7 @@ public abstract class Transaction {
|
|||||||
DEPLOY_AT(16, true),
|
DEPLOY_AT(16, true),
|
||||||
MESSAGE(17, true),
|
MESSAGE(17, true),
|
||||||
CHAT(18, false),
|
CHAT(18, false),
|
||||||
SUPERNODE(19, false),
|
PUBLICIZE(19, false),
|
||||||
AIRDROP(20, false),
|
AIRDROP(20, false),
|
||||||
AT(21, false),
|
AT(21, false),
|
||||||
CREATE_GROUP(22, true),
|
CREATE_GROUP(22, true),
|
||||||
@ -242,6 +242,7 @@ public abstract class Transaction {
|
|||||||
SELF_SHARE_EXISTS(91),
|
SELF_SHARE_EXISTS(91),
|
||||||
ACCOUNT_ALREADY_EXISTS(92),
|
ACCOUNT_ALREADY_EXISTS(92),
|
||||||
INVALID_GROUP_BLOCK_DELAY(93),
|
INVALID_GROUP_BLOCK_DELAY(93),
|
||||||
|
INCORRECT_NONCE(94),
|
||||||
CHAT(999),
|
CHAT(999),
|
||||||
NOT_YET_RELEASED(1000);
|
NOT_YET_RELEASED(1000);
|
||||||
|
|
||||||
|
@ -0,0 +1,94 @@
|
|||||||
|
package org.qortal.transform.transaction;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
import org.qortal.data.transaction.BaseTransactionData;
|
||||||
|
import org.qortal.data.transaction.PublicizeTransactionData;
|
||||||
|
import org.qortal.data.transaction.TransactionData;
|
||||||
|
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 PublicizeTransactionTransformer extends TransactionTransformer {
|
||||||
|
|
||||||
|
// Property lengths
|
||||||
|
private static final int NONCE_LENGTH = INT_LENGTH;
|
||||||
|
|
||||||
|
private static final int EXTRAS_LENGTH = NONCE_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("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();
|
||||||
|
|
||||||
|
long fee = byteBuffer.getLong();
|
||||||
|
|
||||||
|
byte[] signature = new byte[SIGNATURE_LENGTH];
|
||||||
|
byteBuffer.get(signature);
|
||||||
|
|
||||||
|
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, senderPublicKey, fee, signature);
|
||||||
|
|
||||||
|
return new PublicizeTransactionData(baseTransactionData, nonce);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getDataLength(TransactionData transactionData) {
|
||||||
|
return getBaseLength(transactionData) + EXTRAS_LENGTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] toBytes(TransactionData transactionData) throws TransformationException {
|
||||||
|
try {
|
||||||
|
PublicizeTransactionData publicizeTransactionData = (PublicizeTransactionData) transactionData;
|
||||||
|
|
||||||
|
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
transformCommonBytes(transactionData, bytes);
|
||||||
|
|
||||||
|
bytes.write(Ints.toByteArray(publicizeTransactionData.getNonce()));
|
||||||
|
|
||||||
|
bytes.write(Longs.toByteArray(publicizeTransactionData.getFee()));
|
||||||
|
|
||||||
|
if (publicizeTransactionData.getSignature() != null)
|
||||||
|
bytes.write(publicizeTransactionData.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -48,7 +48,7 @@ public class SerializationTests extends Common {
|
|||||||
case ACCOUNT_FLAGS:
|
case ACCOUNT_FLAGS:
|
||||||
case AT:
|
case AT:
|
||||||
case CHAT:
|
case CHAT:
|
||||||
case SUPERNODE:
|
case PUBLICIZE:
|
||||||
case AIRDROP:
|
case AIRDROP:
|
||||||
case ENABLE_FORGING:
|
case ENABLE_FORGING:
|
||||||
continue;
|
continue;
|
||||||
|
Loading…
Reference in New Issue
Block a user