diff --git a/src/main/java/org/qortal/api/resource/AddressesResource.java b/src/main/java/org/qortal/api/resource/AddressesResource.java index bda95bea..39b4bd71 100644 --- a/src/main/java/org/qortal/api/resource/AddressesResource.java +++ b/src/main/java/org/qortal/api/resource/AddressesResource.java @@ -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); + } + } + } diff --git a/src/main/java/org/qortal/api/resource/TransactionsResource.java b/src/main/java/org/qortal/api/resource/TransactionsResource.java index e09fc41c..202ce258 100644 --- a/src/main/java/org/qortal/api/resource/TransactionsResource.java +++ b/src/main/java/org/qortal/api/resource/TransactionsResource.java @@ -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 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 signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, + publicKey, confirmationStatus, limit, offset, reverse); + + // Expand signatures to transactions + List 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( diff --git a/src/main/java/org/qortal/data/transaction/PublicizeTransactionData.java b/src/main/java/org/qortal/data/transaction/PublicizeTransactionData.java new file mode 100644 index 00000000..cdb9129d --- /dev/null +++ b/src/main/java/org/qortal/data/transaction/PublicizeTransactionData.java @@ -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; + } + +} diff --git a/src/main/java/org/qortal/repository/TransactionRepository.java b/src/main/java/org/qortal/repository/TransactionRepository.java index d519b76a..56e51be1 100644 --- a/src/main/java/org/qortal/repository/TransactionRepository.java +++ b/src/main/java/org/qortal/repository/TransactionRepository.java @@ -72,6 +72,24 @@ public interface TransactionRepository { List txTypes, Integer service, String address, ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse) throws DataException; + /** + * Returns signatures for transactions that match search criteria. + *

+ * 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 getSignaturesMatchingCriteria(TransactionType txType, byte[] publicKey, + ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse) throws DataException; + /** * Returns signature for latest auto-update transaction. *

diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java index 42447232..037d9c85 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -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; diff --git a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBPublicizeTransactionRepository.java b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBPublicizeTransactionRepository.java new file mode 100644 index 00000000..d67c61b4 --- /dev/null +++ b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBPublicizeTransactionRepository.java @@ -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); + } + } + +} diff --git a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBTransactionRepository.java b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBTransactionRepository.java index adcc09c3..0ab6ed94 100644 --- a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBTransactionRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBTransactionRepository.java @@ -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 getSignaturesMatchingCriteria(TransactionType txType, byte[] publicKey, + ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse) throws DataException { + List signatures = new ArrayList<>(); + + StringBuilder sql = new StringBuilder(1024); + sql.append("SELECT signature FROM Transactions "); + + List whereClauses = new ArrayList<>(); + List 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) diff --git a/src/main/java/org/qortal/transaction/PublicizeTransaction.java b/src/main/java/org/qortal/transaction/PublicizeTransaction.java new file mode 100644 index 00000000..fbbfbb52 --- /dev/null +++ b/src/main/java/org/qortal/transaction/PublicizeTransaction.java @@ -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 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 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 */ + } + +} diff --git a/src/main/java/org/qortal/transaction/Transaction.java b/src/main/java/org/qortal/transaction/Transaction.java index ffe65cdf..bdddfb1a 100644 --- a/src/main/java/org/qortal/transaction/Transaction.java +++ b/src/main/java/org/qortal/transaction/Transaction.java @@ -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); diff --git a/src/main/java/org/qortal/transform/transaction/PublicizeTransactionTransformer.java b/src/main/java/org/qortal/transform/transaction/PublicizeTransactionTransformer.java new file mode 100644 index 00000000..4a4418a1 --- /dev/null +++ b/src/main/java/org/qortal/transform/transaction/PublicizeTransactionTransformer.java @@ -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; + } + +} diff --git a/src/test/java/org/qortal/test/SerializationTests.java b/src/test/java/org/qortal/test/SerializationTests.java index 24c5fc72..0632495f 100644 --- a/src/test/java/org/qortal/test/SerializationTests.java +++ b/src/test/java/org/qortal/test/SerializationTests.java @@ -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;