Refactor to move arbitrary transaction building to its own class.

This commit is contained in:
CalDescent 2021-10-24 17:41:28 +01:00
parent 305e0f1772
commit 6c01955561
3 changed files with 144 additions and 151 deletions

View File

@ -27,6 +27,7 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.api.*;
import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
import org.qortal.arbitrary.ArbitraryDataTransactionBuilder;
import org.qortal.block.BlockChain;
import org.qortal.crypto.Crypto;
import org.qortal.data.PaymentData;
@ -255,7 +256,10 @@ public class ArbitraryResource {
}
)
@ApiErrors({ApiError.REPOSITORY_ISSUE})
public String uploadFileAtPath(@PathParam("publickey") String creatorPublicKeyBase58, String path) {
public String uploadFileAtPath(@PathParam("method") String methodString,
@PathParam("publickey") String publicKey58,
@PathParam("name") String name,
String path) {
Security.checkApiCallAllowed(request);
// It's too dangerous to allow user-supplied filenames in weaker security contexts
@ -263,84 +267,16 @@ public class ArbitraryResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
}
ArbitraryDataFile arbitraryDataFile = null;
try (final Repository repository = RepositoryManager.getRepository()) {
try {
ArbitraryDataTransactionBuilder transactionBuilder = new ArbitraryDataTransactionBuilder(
publicKey58, Paths.get(path), name, Method.valueOf(methodString), Service.ARBITRARY_DATA
);
if (creatorPublicKeyBase58 == null || path == null) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
}
byte[] creatorPublicKey = Base58.decode(creatorPublicKeyBase58);
final String creatorAddress = Crypto.toAddress(creatorPublicKey);
byte[] lastReference = repository.getAccountRepository().getLastReference(creatorAddress);
if (lastReference == null) {
// Use a random last reference on the very first transaction for an account
// Code copied from CrossChainResource.buildAtMessage()
// We already require PoW on all arbitrary transactions, so no additional logic is needed
Random random = new Random();
lastReference = new byte[Transformer.SIGNATURE_LENGTH];
random.nextBytes(lastReference);
}
String name = null;
byte[] secret = null;
Method method = Method.PUT;
Service service = Service.ARBITRARY_DATA;
Compression compression = Compression.NONE;
ArbitraryDataWriter arbitraryDataWriter = new ArbitraryDataWriter(Paths.get(path), name, service, method, compression);
try {
arbitraryDataWriter.save();
} catch (IOException | DataException | InterruptedException e) {
LOGGER.info("Unable to create arbitrary data file: {}", e.getMessage());
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE);
} catch (RuntimeException e) {
LOGGER.info("Unable to create arbitrary data file: {}", e.getMessage());
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
}
String digest58 = arbitraryDataFile.digest58();
if (digest58 == null) {
LOGGER.error("Unable to calculate digest");
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
}
final BaseTransactionData baseTransactionData = new BaseTransactionData(NTP.getTime(), Group.NO_GROUP,
lastReference, creatorPublicKey, BlockChain.getInstance().getUnitFee(), null);
final int size = (int) arbitraryDataFile.size();
final int version = 5;
final int nonce = 0;
final ArbitraryTransactionData.DataType dataType = ArbitraryTransactionData.DataType.DATA_HASH;
final byte[] digest = arbitraryDataFile.digest();
final byte[] chunkHashes = arbitraryDataFile.chunkHashes();
final List<PaymentData> payments = new ArrayList<>();
ArbitraryTransactionData transactionData = new ArbitraryTransactionData(baseTransactionData,
version, service, nonce, size, name, method,
secret, compression, digest, dataType, chunkHashes, payments);
ArbitraryTransaction transaction = (ArbitraryTransaction) Transaction.fromData(repository, transactionData);
transaction.computeNonce();
Transaction.ValidationResult result = transaction.isValidUnconfirmed();
if (result != Transaction.ValidationResult.OK) {
arbitraryDataFile.deleteAll();
throw TransactionsResource.createTransactionInvalidException(request, result);
}
byte[] bytes = ArbitraryTransactionTransformer.toBytes(transactionData);
byte[] bytes = transactionBuilder.build();
return Base58.encode(bytes);
} catch (DataException e) {
arbitraryDataFile.deleteAll();
LOGGER.error("Repository issue when uploading data", e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
} catch (TransformationException e) {
arbitraryDataFile.deleteAll();
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
} catch (IllegalStateException e) {
arbitraryDataFile.deleteAll();
LOGGER.error("Invalid upload data", e);
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA, e);
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_DATA, e.getMessage());
}
}

View File

@ -31,6 +31,7 @@ import org.qortal.api.ApiError;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.HTMLParser;
import org.qortal.api.Security;
import org.qortal.arbitrary.ArbitraryDataTransactionBuilder;
import org.qortal.block.BlockChain;
import org.qortal.crypto.Crypto;
import org.qortal.data.PaymentData;
@ -91,92 +92,27 @@ public class WebsiteResource {
)
}
)
public String uploadWebsite(@PathParam("method") String methodString, @PathParam("publickey") String publicKey58, @PathParam("name") String name, String path) {
public String uploadWebsite(@PathParam("method") String methodString,
@PathParam("publickey") String publicKey58,
@PathParam("name") String name,
String path) {
Security.checkApiCallAllowed(request);
// It's too dangerous to allow user-supplied filenames in weaker security contexts
// It's too dangerous to allow user-supplied file paths in weaker security contexts
if (Settings.getInstance().isApiRestricted()) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
}
ArbitraryDataFile arbitraryDataFile = null;
try (final Repository repository = RepositoryManager.getRepository()) {
try {
ArbitraryDataTransactionBuilder transactionBuilder = new ArbitraryDataTransactionBuilder(
publicKey58, Paths.get(path), name, Method.valueOf(methodString), Service.WEBSITE
);
if (publicKey58 == null || path == null) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
}
byte[] creatorPublicKey = Base58.decode(publicKey58);
final String creatorAddress = Crypto.toAddress(creatorPublicKey);
byte[] lastReference = repository.getAccountRepository().getLastReference(creatorAddress);
if (lastReference == null) {
// Use a random last reference on the very first transaction for an account
// Code copied from CrossChainResource.buildAtMessage()
// We already require PoW on all arbitrary transactions, so no additional logic is needed
Random random = new Random();
lastReference = new byte[Transformer.SIGNATURE_LENGTH];
random.nextBytes(lastReference);
}
ArbitraryTransactionData.Method method = ArbitraryTransactionData.Method.valueOf(methodString);
ArbitraryTransactionData.Service service = ArbitraryTransactionData.Service.WEBSITE;
ArbitraryTransactionData.Compression compression = ArbitraryTransactionData.Compression.ZIP;
ArbitraryDataWriter arbitraryDataWriter = new ArbitraryDataWriter(Paths.get(path), name, service, method, compression);
try {
arbitraryDataWriter.save();
} catch (IOException | DataException | InterruptedException e) {
LOGGER.info("Unable to create arbitrary data file: {}", e.getMessage());
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE);
} catch (RuntimeException e) {
LOGGER.info("Unable to create arbitrary data file: {}", e.getMessage());
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
}
arbitraryDataFile = arbitraryDataWriter.getArbitraryDataFile();
if (arbitraryDataFile == null) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
}
String digest58 = arbitraryDataFile.digest58();
if (digest58 == null) {
LOGGER.error("Unable to calculate digest");
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
}
final BaseTransactionData baseTransactionData = new BaseTransactionData(NTP.getTime(), Group.NO_GROUP,
lastReference, creatorPublicKey, BlockChain.getInstance().getUnitFee(), null);
final int size = (int) arbitraryDataFile.size();
final int version = 5;
final int nonce = 0;
byte[] secret = arbitraryDataFile.getSecret();
final ArbitraryTransactionData.DataType dataType = ArbitraryTransactionData.DataType.DATA_HASH;
final byte[] digest = arbitraryDataFile.digest();
final byte[] chunkHashes = arbitraryDataFile.chunkHashes();
final List<PaymentData> payments = new ArrayList<>();
ArbitraryTransactionData transactionData = new ArbitraryTransactionData(baseTransactionData,
version, service, nonce, size, name, method,
secret, compression, digest, dataType, chunkHashes, payments);
ArbitraryTransaction transaction = (ArbitraryTransaction) Transaction.fromData(repository, transactionData);
LOGGER.info("Computing nonce...");
transaction.computeNonce();
Transaction.ValidationResult result = transaction.isValidUnconfirmed();
if (result != Transaction.ValidationResult.OK) {
arbitraryDataFile.deleteAll();
throw TransactionsResource.createTransactionInvalidException(request, result);
}
byte[] bytes = ArbitraryTransactionTransformer.toBytes(transactionData);
byte[] bytes = transactionBuilder.build();
return Base58.encode(bytes);
} catch (TransformationException e) {
arbitraryDataFile.deleteAll();
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
} catch (DataException e) {
arbitraryDataFile.deleteAll();
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_DATA, e.getMessage());
}
}

View File

@ -0,0 +1,121 @@
package org.qortal.arbitrary;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.block.BlockChain;
import org.qortal.crypto.Crypto;
import org.qortal.data.PaymentData;
import org.qortal.data.transaction.ArbitraryTransactionData;
import org.qortal.data.transaction.ArbitraryTransactionData.*;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.transaction.ArbitraryTransaction;
import org.qortal.transaction.Transaction;
import org.qortal.transform.TransformationException;
import org.qortal.transform.Transformer;
import org.qortal.transform.transaction.ArbitraryTransactionTransformer;
import org.qortal.utils.Base58;
import org.qortal.utils.NTP;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class ArbitraryDataTransactionBuilder {
private static final Logger LOGGER = LogManager.getLogger(ArbitraryDataTransactionBuilder.class);
private String publicKey58;
private Path path;
private String name;
private Method method;
private Service service;
public ArbitraryDataTransactionBuilder(String publicKey58, Path path, String name, Method method, Service service) {
this.publicKey58 = publicKey58;
this.path = path;
this.name = name;
this.method = method;
this.service = service;
}
public byte[] build() throws DataException {
ArbitraryDataFile arbitraryDataFile = null;
try (final Repository repository = RepositoryManager.getRepository()) {
if (publicKey58 == null || path == null) {
throw new DataException("Missing public key or path");
}
byte[] creatorPublicKey = Base58.decode(publicKey58);
final String creatorAddress = Crypto.toAddress(creatorPublicKey);
byte[] lastReference = repository.getAccountRepository().getLastReference(creatorAddress);
if (lastReference == null) {
// Use a random last reference on the very first transaction for an account
// Code copied from CrossChainResource.buildAtMessage()
// We already require PoW on all arbitrary transactions, so no additional logic is needed
Random random = new Random();
lastReference = new byte[Transformer.SIGNATURE_LENGTH];
random.nextBytes(lastReference);
}
ArbitraryTransactionData.Compression compression = ArbitraryTransactionData.Compression.ZIP;
ArbitraryDataWriter arbitraryDataWriter = new ArbitraryDataWriter(path, name, service, method, compression);
try {
arbitraryDataWriter.save();
} catch (IOException | DataException | InterruptedException | RuntimeException e) {
LOGGER.info("Unable to create arbitrary data file: {}", e.getMessage());
throw new DataException(String.format("Unable to create arbitrary data file: %s", e.getMessage()));
}
arbitraryDataFile = arbitraryDataWriter.getArbitraryDataFile();
if (arbitraryDataFile == null) {
throw new DataException("Arbitrary data file is null");
}
String digest58 = arbitraryDataFile.digest58();
if (digest58 == null) {
LOGGER.error("Unable to calculate file digest");
throw new DataException("Unable to calculate file digest");
}
final BaseTransactionData baseTransactionData = new BaseTransactionData(NTP.getTime(), Group.NO_GROUP,
lastReference, creatorPublicKey, BlockChain.getInstance().getUnitFee(), null);
final int size = (int) arbitraryDataFile.size();
final int version = 5;
final int nonce = 0;
byte[] secret = arbitraryDataFile.getSecret();
final ArbitraryTransactionData.DataType dataType = ArbitraryTransactionData.DataType.DATA_HASH;
final byte[] digest = arbitraryDataFile.digest();
final byte[] chunkHashes = arbitraryDataFile.chunkHashes();
final List<PaymentData> payments = new ArrayList<>();
ArbitraryTransactionData transactionData = new ArbitraryTransactionData(baseTransactionData,
version, service, nonce, size, name, method,
secret, compression, digest, dataType, chunkHashes, payments);
ArbitraryTransaction transaction = (ArbitraryTransaction) Transaction.fromData(repository, transactionData);
LOGGER.info("Computing nonce...");
transaction.computeNonce();
Transaction.ValidationResult result = transaction.isValidUnconfirmed();
if (result != Transaction.ValidationResult.OK) {
arbitraryDataFile.deleteAll();
throw new DataException(String.format("Arbitrary transaction invalid: %s", result));
}
return ArbitraryTransactionTransformer.toBytes(transactionData);
} catch (TransformationException | DataException e) {
arbitraryDataFile.deleteAll();
throw new DataException(String.format("Unable to build ARBITRARY transaction: %s", e.getMessage()));
}
}
}