PUBLICIZE transaction for on-chain record of public key

This commit is contained in:
catbref 2020-05-25 15:20:21 +01:00
parent 59de22883b
commit d50f16b8a9
11 changed files with 603 additions and 3 deletions

View File

@ -28,6 +28,7 @@ import org.qortal.api.ApiError;
import org.qortal.api.ApiErrors;
import org.qortal.api.ApiException;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.Security;
import org.qortal.api.model.ApiOnlineAccount;
import org.qortal.api.model.RewardShareKeyRequest;
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.RewardShareData;
import org.qortal.data.network.OnlineAccountData;
import org.qortal.data.transaction.PublicizeTransactionData;
import org.qortal.data.transaction.RewardShareTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.settings.Settings;
import org.qortal.transaction.PublicizeTransaction;
import org.qortal.transaction.Transaction;
import org.qortal.transaction.Transaction.TransactionType;
import org.qortal.transaction.Transaction.ValidationResult;
import org.qortal.transform.TransformationException;
import org.qortal.transform.Transformer;
import org.qortal.transform.transaction.PublicizeTransactionTransformer;
import org.qortal.transform.transaction.RewardShareTransactionTransformer;
import org.qortal.transform.transaction.TransactionTransformer;
import org.qortal.utils.Amounts;
import org.qortal.utils.Base58;
import com.google.common.primitives.Bytes;
@Path("/addresses")
@Tag(name = "Addresses")
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);
}
}
}

View File

@ -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
@Path("/sign")
@Operation(

View File

@ -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;
}
}

View File

@ -72,6 +72,24 @@ public interface TransactionRepository {
List<TransactionType> txTypes, Integer service, String address,
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.
* <p>

View File

@ -613,6 +613,11 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("CREATE INDEX ChatTransactionsRecipientIndex ON ChatTransactions (recipient, sender)");
break;
case 19:
// PUBLICIZE transactions
stmt.execute("CREATE TABLE PublicizeTransactions (signature Signature, nonce INT NOT NULL, " + TRANSACTION_KEYS + ")");
break;
default:
// nothing to do
return false;

View File

@ -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);
}
}
}

View File

@ -499,7 +499,75 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
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())) {
if (resultSet == null)

View 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 */
}
}

View File

@ -62,7 +62,7 @@ public abstract class Transaction {
DEPLOY_AT(16, true),
MESSAGE(17, true),
CHAT(18, false),
SUPERNODE(19, false),
PUBLICIZE(19, false),
AIRDROP(20, false),
AT(21, false),
CREATE_GROUP(22, true),
@ -242,6 +242,7 @@ public abstract class Transaction {
SELF_SHARE_EXISTS(91),
ACCOUNT_ALREADY_EXISTS(92),
INVALID_GROUP_BLOCK_DELAY(93),
INCORRECT_NONCE(94),
CHAT(999),
NOT_YET_RELEASED(1000);

View File

@ -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;
}
}

View File

@ -48,7 +48,7 @@ public class SerializationTests extends Common {
case ACCOUNT_FLAGS:
case AT:
case CHAT:
case SUPERNODE:
case PUBLICIZE:
case AIRDROP:
case ENABLE_FORGING:
continue;