From 4c463f65b77fcc25295142140ba4084910940fdb Mon Sep 17 00:00:00 2001 From: DrewMPeacock Date: Mon, 8 Aug 2022 15:58:46 -0600 Subject: [PATCH 1/9] Add API handles to build CREATE_POLL and VOTE_ON_POLL transactions. --- .../qortal/api/resource/VotingResource.java | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 src/main/java/org/qortal/api/resource/VotingResource.java diff --git a/src/main/java/org/qortal/api/resource/VotingResource.java b/src/main/java/org/qortal/api/resource/VotingResource.java new file mode 100644 index 00000000..bd57c9f7 --- /dev/null +++ b/src/main/java/org/qortal/api/resource/VotingResource.java @@ -0,0 +1,130 @@ +package org.qortal.api.resource; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.qortal.api.ApiError; +import org.qortal.api.ApiErrors; +import org.qortal.api.ApiExceptionFactory; +import org.qortal.data.transaction.CreatePollTransactionData; +import org.qortal.data.transaction.PaymentTransactionData; +import org.qortal.data.transaction.VoteOnPollTransactionData; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; +import org.qortal.settings.Settings; +import org.qortal.transaction.Transaction; +import org.qortal.transform.TransformationException; +import org.qortal.transform.transaction.CreatePollTransactionTransformer; +import org.qortal.transform.transaction.PaymentTransactionTransformer; +import org.qortal.transform.transaction.VoteOnPollTransactionTransformer; +import org.qortal.utils.Base58; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; + +@Path("/Voting") +@Tag(name = "Voting") +public class VotingResource { + @Context + HttpServletRequest request; + + @POST + @Path("/CreatePoll") + @Operation( + summary = "Build raw, unsigned, CREATE_POLL transaction", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = CreatePollTransactionData.class + ) + ) + ), + responses = { + @ApiResponse( + description = "raw, unsigned, CREATE_POLL 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 CreatePoll(CreatePollTransactionData 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); + + Transaction.ValidationResult result = transaction.isValidUnconfirmed(); + if (result != Transaction.ValidationResult.OK) + throw TransactionsResource.createTransactionInvalidException(request, result); + + byte[] bytes = CreatePollTransactionTransformer.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("/VoteOnPoll") + @Operation( + summary = "Build raw, unsigned, VOTE_ON_POLL transaction", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = VoteOnPollTransactionData.class + ) + ) + ), + responses = { + @ApiResponse( + description = "raw, unsigned, VOTE_ON_POLL 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 VoteOnPoll(VoteOnPollTransactionData 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); + + Transaction.ValidationResult result = transaction.isValidUnconfirmed(); + if (result != Transaction.ValidationResult.OK) + throw TransactionsResource.createTransactionInvalidException(request, result); + + byte[] bytes = VoteOnPollTransactionTransformer.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); + } + } + +} From 1abceada209d391f9b466a74b1ba9758e6bc4bf6 Mon Sep 17 00:00:00 2001 From: DrewMPeacock Date: Fri, 9 Sep 2022 11:20:46 -0600 Subject: [PATCH 2/9] Fix up CREATE_POLL and VOTE_ON_POLL transactions to process and validate. Added rule to enforce that a poll creator is also its owner. --- .../CreatePollTransactionData.java | 13 ++++++++++++ .../VoteOnPollTransactionData.java | 3 +++ .../transaction/CreatePollTransaction.java | 21 +++++++++++++------ 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/qortal/data/transaction/CreatePollTransactionData.java b/src/main/java/org/qortal/data/transaction/CreatePollTransactionData.java index 4df7d79d..8b904aa0 100644 --- a/src/main/java/org/qortal/data/transaction/CreatePollTransactionData.java +++ b/src/main/java/org/qortal/data/transaction/CreatePollTransactionData.java @@ -2,9 +2,11 @@ package org.qortal.data.transaction; import java.util.List; +import javax.xml.bind.Unmarshaller; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; +import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorValue; import org.qortal.data.voting.PollOptionData; import org.qortal.transaction.Transaction; import org.qortal.transaction.Transaction.TransactionType; @@ -14,8 +16,13 @@ import io.swagger.v3.oas.annotations.media.Schema; // All properties to be converted to JSON via JAXB @XmlAccessorType(XmlAccessType.FIELD) @Schema(allOf = { TransactionData.class }) +@XmlDiscriminatorValue("CREATE_POLL") public class CreatePollTransactionData extends TransactionData { + + @Schema(description = "Poll creator's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP") + private byte[] pollCreatorPublicKey; + // Properties private String owner; private String pollName; @@ -29,10 +36,15 @@ public class CreatePollTransactionData extends TransactionData { super(TransactionType.CREATE_POLL); } + public void afterUnmarshal(Unmarshaller u, Object parent) { + this.creatorPublicKey = this.pollCreatorPublicKey; + } + public CreatePollTransactionData(BaseTransactionData baseTransactionData, String owner, String pollName, String description, List pollOptions) { super(Transaction.TransactionType.CREATE_POLL, baseTransactionData); + this.creatorPublicKey = baseTransactionData.creatorPublicKey; this.owner = owner; this.pollName = pollName; this.description = description; @@ -41,6 +53,7 @@ public class CreatePollTransactionData extends TransactionData { // Getters/setters + public byte[] getPollCreatorPublicKey() { return this.creatorPublicKey; } public String getOwner() { return this.owner; } diff --git a/src/main/java/org/qortal/data/transaction/VoteOnPollTransactionData.java b/src/main/java/org/qortal/data/transaction/VoteOnPollTransactionData.java index 6145d741..ac467255 100644 --- a/src/main/java/org/qortal/data/transaction/VoteOnPollTransactionData.java +++ b/src/main/java/org/qortal/data/transaction/VoteOnPollTransactionData.java @@ -4,6 +4,7 @@ import javax.xml.bind.Unmarshaller; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; +import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorValue; import org.qortal.transaction.Transaction.TransactionType; import io.swagger.v3.oas.annotations.media.Schema; @@ -11,9 +12,11 @@ import io.swagger.v3.oas.annotations.media.Schema; // All properties to be converted to JSON via JAXB @XmlAccessorType(XmlAccessType.FIELD) @Schema(allOf = { TransactionData.class }) +@XmlDiscriminatorValue("VOTE_ON_POLL") public class VoteOnPollTransactionData extends TransactionData { // Properties + @Schema(description = "Vote creator's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP") private byte[] voterPublicKey; private String pollName; private int optionIndex; diff --git a/src/main/java/org/qortal/transaction/CreatePollTransaction.java b/src/main/java/org/qortal/transaction/CreatePollTransaction.java index a56322a7..1d969965 100644 --- a/src/main/java/org/qortal/transaction/CreatePollTransaction.java +++ b/src/main/java/org/qortal/transaction/CreatePollTransaction.java @@ -51,6 +51,21 @@ public class CreatePollTransaction extends Transaction { if (!Crypto.isValidAddress(this.createPollTransactionData.getOwner())) return ValidationResult.INVALID_ADDRESS; + Account creator = getCreator(); + Account owner = getOwner(); + + String creatorAddress = creator.getAddress(); + String ownerAddress = owner.getAddress(); + + // Check Owner address is the same as the creator public key + if (!creatorAddress.equals(ownerAddress)) { + return ValidationResult.INVALID_ADDRESS; + } + + // Check creator has enough funds + if (creator.getConfirmedBalance(Asset.QORT) < this.createPollTransactionData.getFee()) + return ValidationResult.NO_BALANCE; + // Check name size bounds String pollName = this.createPollTransactionData.getPollName(); int pollNameLength = Utf8.encodedLength(pollName); @@ -88,12 +103,6 @@ public class CreatePollTransaction extends Transaction { optionNames.add(pollOptionData.getOptionName()); } - Account creator = getCreator(); - - // Check creator has enough funds - if (creator.getConfirmedBalance(Asset.QORT) < this.createPollTransactionData.getFee()) - return ValidationResult.NO_BALANCE; - return ValidationResult.OK; } From 5bbde4dcdb1cc8bdd045e018dbaee56e97815829 Mon Sep 17 00:00:00 2001 From: QuickMythril Date: Thu, 6 Apr 2023 04:41:51 -0400 Subject: [PATCH 3/9] Added API calls to get polls & node settings --- src/main/java/org/qortal/api/ApiError.java | 2 +- .../qortal/api/resource/AdminResource.java | 16 +++++ .../qortal/api/resource/VotingResource.java | 67 +++++++++++++++++++ .../data/transaction/TransactionData.java | 2 + .../java/org/qortal/data/voting/PollData.java | 25 +++++++ .../qortal/repository/VotingRepository.java | 2 + .../hsqldb/HSQLDBVotingRepository.java | 49 ++++++++++++++ 7 files changed, 162 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/qortal/api/ApiError.java b/src/main/java/org/qortal/api/ApiError.java index 659104e7..b52332b1 100644 --- a/src/main/java/org/qortal/api/ApiError.java +++ b/src/main/java/org/qortal/api/ApiError.java @@ -79,7 +79,7 @@ public enum ApiError { // BUYER_ALREADY_OWNER(411, 422), // POLLS - // POLL_NO_EXISTS(501, 404), + POLL_NO_EXISTS(501, 404), // POLL_ALREADY_EXISTS(502, 422), // DUPLICATE_OPTION(503, 422), // POLL_OPTION_NO_EXISTS(504, 404), diff --git a/src/main/java/org/qortal/api/resource/AdminResource.java b/src/main/java/org/qortal/api/resource/AdminResource.java index ef2a3f95..154f9159 100644 --- a/src/main/java/org/qortal/api/resource/AdminResource.java +++ b/src/main/java/org/qortal/api/resource/AdminResource.java @@ -153,6 +153,22 @@ public class AdminResource { return nodeStatus; } + @GET + @Path("/settings") + @Operation( + summary = "Fetch node settings", + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = Settings.class)) + ) + } + ) + public Settings settings() { + Settings nodeSettings = Settings.getInstance(); + + return nodeSettings; + } + @GET @Path("/stop") @Operation( diff --git a/src/main/java/org/qortal/api/resource/VotingResource.java b/src/main/java/org/qortal/api/resource/VotingResource.java index bd57c9f7..d98d23a3 100644 --- a/src/main/java/org/qortal/api/resource/VotingResource.java +++ b/src/main/java/org/qortal/api/resource/VotingResource.java @@ -29,12 +29,79 @@ import javax.ws.rs.Path; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import java.util.List; +import javax.ws.rs.GET; +import javax.ws.rs.PathParam; +import javax.ws.rs.QueryParam; +import org.qortal.api.ApiException; +import org.qortal.data.voting.PollData; + @Path("/Voting") @Tag(name = "Voting") public class VotingResource { @Context HttpServletRequest request; + @GET + @Operation( + summary = "List all polls", + responses = { + @ApiResponse( + description = "poll info", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + array = @ArraySchema(schema = @Schema(implementation = PollData.class)) + ) + ) + } + ) + @ApiErrors({ApiError.REPOSITORY_ISSUE}) + public List getAllPolls(@Parameter( + ref = "limit" + ) @QueryParam("limit") Integer limit, @Parameter( + ref = "offset" + ) @QueryParam("offset") Integer offset, @Parameter( + ref = "reverse" + ) @QueryParam("reverse") Boolean reverse) { + try (final Repository repository = RepositoryManager.getRepository()) { + List allPollData = repository.getVotingRepository().getAllPolls(limit, offset, reverse); + return allPollData; + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + + @GET + @Path("/{pollName}") + @Operation( + summary = "Info on poll", + responses = { + @ApiResponse( + description = "poll info", + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = PollData.class) + ) + ) + } + ) + @ApiErrors({ApiError.REPOSITORY_ISSUE}) + public PollData getPollData(@PathParam("pollName") String pollName) { + try (final Repository repository = RepositoryManager.getRepository()) { + PollData pollData = repository.getVotingRepository().fromPollName(pollName); + if (pollData == null) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.POLL_NO_EXISTS); + + return pollData; + } catch (ApiException e) { + throw e; + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + @POST @Path("/CreatePoll") @Operation( diff --git a/src/main/java/org/qortal/data/transaction/TransactionData.java b/src/main/java/org/qortal/data/transaction/TransactionData.java index ec1139f4..838cffd3 100644 --- a/src/main/java/org/qortal/data/transaction/TransactionData.java +++ b/src/main/java/org/qortal/data/transaction/TransactionData.java @@ -12,6 +12,7 @@ import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorNode; import org.qortal.crypto.Crypto; +import org.qortal.data.voting.PollData; import org.qortal.transaction.Transaction.ApprovalStatus; import org.qortal.transaction.Transaction.TransactionType; @@ -29,6 +30,7 @@ import io.swagger.v3.oas.annotations.media.Schema.AccessMode; @XmlSeeAlso({GenesisTransactionData.class, PaymentTransactionData.class, RegisterNameTransactionData.class, UpdateNameTransactionData.class, SellNameTransactionData.class, CancelSellNameTransactionData.class, BuyNameTransactionData.class, CreatePollTransactionData.class, VoteOnPollTransactionData.class, ArbitraryTransactionData.class, + PollData.class, IssueAssetTransactionData.class, TransferAssetTransactionData.class, CreateAssetOrderTransactionData.class, CancelAssetOrderTransactionData.class, MultiPaymentTransactionData.class, DeployAtTransactionData.class, MessageTransactionData.class, ATTransactionData.class, diff --git a/src/main/java/org/qortal/data/voting/PollData.java b/src/main/java/org/qortal/data/voting/PollData.java index 4af62087..1850ddc7 100644 --- a/src/main/java/org/qortal/data/voting/PollData.java +++ b/src/main/java/org/qortal/data/voting/PollData.java @@ -14,6 +14,11 @@ public class PollData { // Constructors + // For JAXB + protected PollData() { + super(); + } + public PollData(byte[] creatorPublicKey, String owner, String pollName, String description, List pollOptions, long published) { this.creatorPublicKey = creatorPublicKey; this.owner = owner; @@ -29,22 +34,42 @@ public class PollData { return this.creatorPublicKey; } + public void setCreatorPublicKey(byte[] creatorPublicKey) { + this.creatorPublicKey = creatorPublicKey; + } + public String getOwner() { return this.owner; } + public void setOwner(String owner) { + this.owner = owner; + } + public String getPollName() { return this.pollName; } + public void setPollName(String pollName) { + this.pollName = pollName; + } + public String getDescription() { return this.description; } + public void setDescription(String description) { + this.description = description; + } + public List getPollOptions() { return this.pollOptions; } + public void setPollOptions(List pollOptions) { + this.pollOptions = pollOptions; + } + public long getPublished() { return this.published; } diff --git a/src/main/java/org/qortal/repository/VotingRepository.java b/src/main/java/org/qortal/repository/VotingRepository.java index 28a9f6c7..b0e2954c 100644 --- a/src/main/java/org/qortal/repository/VotingRepository.java +++ b/src/main/java/org/qortal/repository/VotingRepository.java @@ -9,6 +9,8 @@ public interface VotingRepository { // Polls + public List getAllPolls(Integer limit, Integer offset, Boolean reverse) throws DataException; + public PollData fromPollName(String pollName) throws DataException; public boolean pollExists(String pollName) throws DataException; diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBVotingRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBVotingRepository.java index 447fbe4c..cc33426b 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBVotingRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBVotingRepository.java @@ -21,6 +21,55 @@ public class HSQLDBVotingRepository implements VotingRepository { // Polls + @Override + public List getAllPolls(Integer limit, Integer offset, Boolean reverse) throws DataException { + StringBuilder sql = new StringBuilder(512); + + sql.append("SELECT poll_name, description, creator, owner, published_when FROM Polls ORDER BY poll_name"); + + if (reverse != null && reverse) + sql.append(" DESC"); + + HSQLDBRepository.limitOffsetSql(sql, limit, offset); + + List polls = new ArrayList<>(); + + try (ResultSet resultSet = this.repository.checkedExecute(sql.toString())) { + if (resultSet == null) + return polls; + + do { + String pollName = resultSet.getString(1); + String description = resultSet.getString(2); + byte[] creatorPublicKey = resultSet.getBytes(3); + String owner = resultSet.getString(4); + long published = resultSet.getLong(5); + + String optionsSql = "SELECT option_name FROM PollOptions WHERE poll_name = ? ORDER BY option_index ASC"; + try (ResultSet optionsResultSet = this.repository.checkedExecute(optionsSql, pollName)) { + if (optionsResultSet == null) + return null; + + List pollOptions = new ArrayList<>(); + + // NOTE: do-while because checkedExecute() above has already called rs.next() for us + do { + String optionName = optionsResultSet.getString(1); + + pollOptions.add(new PollOptionData(optionName)); + } while (optionsResultSet.next()); + + polls.add(new PollData(creatorPublicKey, owner, pollName, description, pollOptions, published)); + } + + } while (resultSet.next()); + + return polls; + } catch (SQLException e) { + throw new DataException("Unable to fetch polls from repository", e); + } + } + @Override public PollData fromPollName(String pollName) throws DataException { String sql = "SELECT description, creator, owner, published_when FROM Polls WHERE poll_name = ?"; From 23ec71d7be6b23289275b91c5c88a1846e5938bc Mon Sep 17 00:00:00 2001 From: QuickMythril Date: Thu, 6 Apr 2023 04:48:18 -0400 Subject: [PATCH 4/9] Renamed API calls from "voting" to "polls" --- .../{VotingResource.java => PollsResource.java} | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) rename src/main/java/org/qortal/api/resource/{VotingResource.java => PollsResource.java} (98%) diff --git a/src/main/java/org/qortal/api/resource/VotingResource.java b/src/main/java/org/qortal/api/resource/PollsResource.java similarity index 98% rename from src/main/java/org/qortal/api/resource/VotingResource.java rename to src/main/java/org/qortal/api/resource/PollsResource.java index d98d23a3..952cbdc5 100644 --- a/src/main/java/org/qortal/api/resource/VotingResource.java +++ b/src/main/java/org/qortal/api/resource/PollsResource.java @@ -38,9 +38,9 @@ import javax.ws.rs.QueryParam; import org.qortal.api.ApiException; import org.qortal.data.voting.PollData; -@Path("/Voting") -@Tag(name = "Voting") -public class VotingResource { +@Path("/polls") +@Tag(name = "Polls") +public class PollsResource { @Context HttpServletRequest request; @@ -103,7 +103,7 @@ public class VotingResource { } @POST - @Path("/CreatePoll") + @Path("/create") @Operation( summary = "Build raw, unsigned, CREATE_POLL transaction", requestBody = @RequestBody( @@ -149,7 +149,7 @@ public class VotingResource { } @POST - @Path("/VoteOnPoll") + @Path("/vote") @Operation( summary = "Build raw, unsigned, VOTE_ON_POLL transaction", requestBody = @RequestBody( From 57485bfe3604502e122210bfefac80f140538e0f Mon Sep 17 00:00:00 2001 From: QuickMythril Date: Sat, 15 Apr 2023 09:11:27 -0400 Subject: [PATCH 5/9] Removed check from poll tx that creator is owner --- .../transaction/CreatePollTransaction.java | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/qortal/transaction/CreatePollTransaction.java b/src/main/java/org/qortal/transaction/CreatePollTransaction.java index 1d969965..a56322a7 100644 --- a/src/main/java/org/qortal/transaction/CreatePollTransaction.java +++ b/src/main/java/org/qortal/transaction/CreatePollTransaction.java @@ -51,21 +51,6 @@ public class CreatePollTransaction extends Transaction { if (!Crypto.isValidAddress(this.createPollTransactionData.getOwner())) return ValidationResult.INVALID_ADDRESS; - Account creator = getCreator(); - Account owner = getOwner(); - - String creatorAddress = creator.getAddress(); - String ownerAddress = owner.getAddress(); - - // Check Owner address is the same as the creator public key - if (!creatorAddress.equals(ownerAddress)) { - return ValidationResult.INVALID_ADDRESS; - } - - // Check creator has enough funds - if (creator.getConfirmedBalance(Asset.QORT) < this.createPollTransactionData.getFee()) - return ValidationResult.NO_BALANCE; - // Check name size bounds String pollName = this.createPollTransactionData.getPollName(); int pollNameLength = Utf8.encodedLength(pollName); @@ -103,6 +88,12 @@ public class CreatePollTransaction extends Transaction { optionNames.add(pollOptionData.getOptionName()); } + Account creator = getCreator(); + + // Check creator has enough funds + if (creator.getConfirmedBalance(Asset.QORT) < this.createPollTransactionData.getFee()) + return ValidationResult.NO_BALANCE; + return ValidationResult.OK; } From 735de93848dee8c3382ae27c7aaf676754277167 Mon Sep 17 00:00:00 2001 From: QuickMythril Date: Sat, 15 Apr 2023 09:25:28 -0400 Subject: [PATCH 6/9] Removed internal use parameter from API endpoint --- .../qortal/data/transaction/VoteOnPollTransactionData.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/org/qortal/data/transaction/VoteOnPollTransactionData.java b/src/main/java/org/qortal/data/transaction/VoteOnPollTransactionData.java index ac467255..a23d5e2b 100644 --- a/src/main/java/org/qortal/data/transaction/VoteOnPollTransactionData.java +++ b/src/main/java/org/qortal/data/transaction/VoteOnPollTransactionData.java @@ -3,6 +3,7 @@ package org.qortal.data.transaction; import javax.xml.bind.Unmarshaller; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlTransient; import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorValue; import org.qortal.transaction.Transaction.TransactionType; @@ -20,6 +21,9 @@ public class VoteOnPollTransactionData extends TransactionData { private byte[] voterPublicKey; private String pollName; private int optionIndex; + // For internal use when orphaning + @XmlTransient + @Schema(hidden = true) private Integer previousOptionIndex; // Constructors From e041748b4870529871700fee1e29703c6d869b52 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Sun, 16 Apr 2023 13:59:25 +0100 Subject: [PATCH 7/9] Improved name rebuilding code, to handle some more complex scenarios. --- .../NamesDatabaseIntegrityCheck.java | 118 +++++++++++++----- .../qortal/test/naming/IntegrityTests.java | 2 +- 2 files changed, 85 insertions(+), 35 deletions(-) diff --git a/src/main/java/org/qortal/controller/repository/NamesDatabaseIntegrityCheck.java b/src/main/java/org/qortal/controller/repository/NamesDatabaseIntegrityCheck.java index 004fa692..99eaf105 100644 --- a/src/main/java/org/qortal/controller/repository/NamesDatabaseIntegrityCheck.java +++ b/src/main/java/org/qortal/controller/repository/NamesDatabaseIntegrityCheck.java @@ -13,7 +13,9 @@ import org.qortal.repository.RepositoryManager; import org.qortal.transaction.Transaction.TransactionType; import org.qortal.utils.Unicode; +import java.math.BigInteger; import java.util.*; +import java.util.stream.Collectors; public class NamesDatabaseIntegrityCheck { @@ -28,16 +30,8 @@ public class NamesDatabaseIntegrityCheck { private List nameTransactions = new ArrayList<>(); + public int rebuildName(String name, Repository repository) { - return this.rebuildName(name, repository, null); - } - - public int rebuildName(String name, Repository repository, List referenceNames) { - // "referenceNames" tracks the linked names that have already been rebuilt, to prevent circular dependencies - if (referenceNames == null) { - referenceNames = new ArrayList<>(); - } - int modificationCount = 0; try { List transactions = this.fetchAllTransactionsInvolvingName(name, repository); @@ -46,6 +40,14 @@ public class NamesDatabaseIntegrityCheck { return modificationCount; } + // If this name has been updated at any point, we need to add transactions from the other names to the sequence + int added = this.addAdditionalTransactionsRelatingToName(transactions, name, repository); + while (added > 0) { + // Keep going until all have been added + LOGGER.trace("{} added for {}. Looking for more transactions...", added, name); + added = this.addAdditionalTransactionsRelatingToName(transactions, name, repository); + } + // Loop through each past transaction and re-apply it to the Names table for (TransactionData currentTransaction : transactions) { @@ -61,29 +63,14 @@ public class NamesDatabaseIntegrityCheck { // Process UPDATE_NAME transactions if (currentTransaction.getType() == TransactionType.UPDATE_NAME) { UpdateNameTransactionData updateNameTransactionData = (UpdateNameTransactionData) currentTransaction; - - if (Objects.equals(updateNameTransactionData.getNewName(), name) && - !Objects.equals(updateNameTransactionData.getName(), updateNameTransactionData.getNewName())) { - // This renames an existing name, so we need to process that instead - - if (!referenceNames.contains(name)) { - referenceNames.add(name); - this.rebuildName(updateNameTransactionData.getName(), repository, referenceNames); - } - else { - // We've already processed this name so there's nothing more to do - } - } - else { - Name nameObj = new Name(repository, name); - if (nameObj != null && nameObj.getNameData() != null) { - nameObj.update(updateNameTransactionData); - modificationCount++; - LOGGER.trace("Processed UPDATE_NAME transaction for name {}", name); - } else { - // Something went wrong - throw new DataException(String.format("Name data not found for name %s", updateNameTransactionData.getName())); - } + Name nameObj = new Name(repository, updateNameTransactionData.getName()); + if (nameObj != null && nameObj.getNameData() != null) { + nameObj.update(updateNameTransactionData); + modificationCount++; + LOGGER.trace("Processed UPDATE_NAME transaction for name {}", name); + } else { + // Something went wrong + throw new DataException(String.format("Name data not found for name %s", updateNameTransactionData.getName())); } } @@ -354,8 +341,8 @@ public class NamesDatabaseIntegrityCheck { } } - // Sort by lowest timestamp first - transactions.sort(Comparator.comparingLong(TransactionData::getTimestamp)); + // Sort by lowest block height first + sortTransactions(transactions); return transactions; } @@ -419,4 +406,67 @@ public class NamesDatabaseIntegrityCheck { return names; } + private int addAdditionalTransactionsRelatingToName(List transactions, String name, Repository repository) throws DataException { + int added = 0; + + // If this name has been updated at any point, we need to add transactions from the other names to the sequence + List otherNames = new ArrayList<>(); + List updateNameTransactions = transactions.stream().filter(t -> t.getType() == TransactionType.UPDATE_NAME).collect(Collectors.toList()); + for (TransactionData transactionData : updateNameTransactions) { + UpdateNameTransactionData updateNameTransactionData = (UpdateNameTransactionData) transactionData; + // If the newName field isn't empty, and either the "name" or "newName" is different from our reference name, + // we should remember this additional name, in case it has relevant transactions associated with it. + if (updateNameTransactionData.getNewName() != null && !updateNameTransactionData.getNewName().isEmpty()) { + if (!Objects.equals(updateNameTransactionData.getName(), name)) { + otherNames.add(updateNameTransactionData.getName()); + } + if (!Objects.equals(updateNameTransactionData.getNewName(), name)) { + otherNames.add(updateNameTransactionData.getNewName()); + } + } + } + + + for (String otherName : otherNames) { + List otherNameTransactions = this.fetchAllTransactionsInvolvingName(otherName, repository); + for (TransactionData otherNameTransactionData : otherNameTransactions) { + if (!transactions.contains(otherNameTransactionData)) { + // Add new transaction relating to other name + transactions.add(otherNameTransactionData); + added++; + } + } + } + + if (added > 0) { + // New transaction(s) added, so re-sort + sortTransactions(transactions); + } + + return added; + } + + private void sortTransactions(List transactions) { + Collections.sort(transactions, new Comparator() { + public int compare(Object o1, Object o2) { + TransactionData td1 = (TransactionData) o1; + TransactionData td2 = (TransactionData) o2; + + // Sort by block height first + int heightComparison = td1.getBlockHeight().compareTo(td2.getBlockHeight()); + if (heightComparison != 0) { + return heightComparison; + } + + // Same height so compare timestamps + int timestampComparison = Long.compare(td1.getTimestamp(), td2.getTimestamp()); + if (timestampComparison != 0) { + return timestampComparison; + } + + // Same timestamp so compare signatures + return new BigInteger(td1.getSignature()).compareTo(new BigInteger(td2.getSignature())); + }}); + } + } diff --git a/src/test/java/org/qortal/test/naming/IntegrityTests.java b/src/test/java/org/qortal/test/naming/IntegrityTests.java index d52d4983..767ea388 100644 --- a/src/test/java/org/qortal/test/naming/IntegrityTests.java +++ b/src/test/java/org/qortal/test/naming/IntegrityTests.java @@ -128,7 +128,7 @@ public class IntegrityTests extends Common { // Run the database integrity check for the initial name, to ensure it doesn't get into a loop NamesDatabaseIntegrityCheck integrityCheck = new NamesDatabaseIntegrityCheck(); - assertEquals(2, integrityCheck.rebuildName(initialName, repository)); + assertEquals(4, integrityCheck.rebuildName(initialName, repository)); // 4 transactions total // Ensure the new name still exists and the data is still correct assertTrue(repository.getNameRepository().nameExists(initialName)); From 8331241d7582249f78a96b50cf3851b80d180e5f Mon Sep 17 00:00:00 2001 From: CalDescent Date: Tue, 18 Apr 2023 19:01:45 +0100 Subject: [PATCH 8/9] Bump version to 3.9.1 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 3e59c66d..083901a6 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.qortal qortal - 3.9.0 + 3.9.1 jar true From 358e67b05061849a5e4b148beaa185cca6a9dc75 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Wed, 19 Apr 2023 20:56:47 +0100 Subject: [PATCH 9/9] Added "bindAddressFallback" setting, which defaults to "0.0.0.0". Should fix problems on systems unable to use IPv6 wildcard (::) for listening, and avoids having to manually specify "bindAddress": "0.0.0.0" in settings.json. --- src/main/java/org/qortal/api/ApiService.java | 5 +- .../java/org/qortal/api/DomainMapService.java | 5 +- .../java/org/qortal/api/GatewayService.java | 5 +- src/main/java/org/qortal/network/Network.java | 60 +++++++++++++------ .../java/org/qortal/settings/Settings.java | 5 ++ 5 files changed, 56 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/qortal/api/ApiService.java b/src/main/java/org/qortal/api/ApiService.java index 78c9250c..f74082f2 100644 --- a/src/main/java/org/qortal/api/ApiService.java +++ b/src/main/java/org/qortal/api/ApiService.java @@ -41,6 +41,7 @@ import org.glassfish.jersey.servlet.ServletContainer; import org.qortal.api.resource.AnnotationPostProcessor; import org.qortal.api.resource.ApiDefinition; import org.qortal.api.websocket.*; +import org.qortal.network.Network; import org.qortal.settings.Settings; public class ApiService { @@ -123,13 +124,13 @@ public class ApiService { ServerConnector portUnifiedConnector = new ServerConnector(this.server, new DetectorConnectionFactory(sslConnectionFactory), httpConnectionFactory); - portUnifiedConnector.setHost(Settings.getInstance().getBindAddress()); + portUnifiedConnector.setHost(Network.getInstance().getBindAddress()); portUnifiedConnector.setPort(Settings.getInstance().getApiPort()); this.server.addConnector(portUnifiedConnector); } else { // Non-SSL - InetAddress bindAddr = InetAddress.getByName(Settings.getInstance().getBindAddress()); + InetAddress bindAddr = InetAddress.getByName(Network.getInstance().getBindAddress()); InetSocketAddress endpoint = new InetSocketAddress(bindAddr, Settings.getInstance().getApiPort()); this.server = new Server(endpoint); } diff --git a/src/main/java/org/qortal/api/DomainMapService.java b/src/main/java/org/qortal/api/DomainMapService.java index ba0fa067..a2678e38 100644 --- a/src/main/java/org/qortal/api/DomainMapService.java +++ b/src/main/java/org/qortal/api/DomainMapService.java @@ -16,6 +16,7 @@ import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.servlet.ServletContainer; import org.qortal.api.resource.AnnotationPostProcessor; import org.qortal.api.resource.ApiDefinition; +import org.qortal.network.Network; import org.qortal.settings.Settings; import javax.net.ssl.KeyManagerFactory; @@ -99,13 +100,13 @@ public class DomainMapService { ServerConnector portUnifiedConnector = new ServerConnector(this.server, new DetectorConnectionFactory(sslConnectionFactory), httpConnectionFactory); - portUnifiedConnector.setHost(Settings.getInstance().getBindAddress()); + portUnifiedConnector.setHost(Network.getInstance().getBindAddress()); portUnifiedConnector.setPort(Settings.getInstance().getDomainMapPort()); this.server.addConnector(portUnifiedConnector); } else { // Non-SSL - InetAddress bindAddr = InetAddress.getByName(Settings.getInstance().getBindAddress()); + InetAddress bindAddr = InetAddress.getByName(Network.getInstance().getBindAddress()); InetSocketAddress endpoint = new InetSocketAddress(bindAddr, Settings.getInstance().getDomainMapPort()); this.server = new Server(endpoint); } diff --git a/src/main/java/org/qortal/api/GatewayService.java b/src/main/java/org/qortal/api/GatewayService.java index 030a0f2f..6625ed0a 100644 --- a/src/main/java/org/qortal/api/GatewayService.java +++ b/src/main/java/org/qortal/api/GatewayService.java @@ -15,6 +15,7 @@ import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.servlet.ServletContainer; import org.qortal.api.resource.AnnotationPostProcessor; import org.qortal.api.resource.ApiDefinition; +import org.qortal.network.Network; import org.qortal.settings.Settings; import javax.net.ssl.KeyManagerFactory; @@ -98,13 +99,13 @@ public class GatewayService { ServerConnector portUnifiedConnector = new ServerConnector(this.server, new DetectorConnectionFactory(sslConnectionFactory), httpConnectionFactory); - portUnifiedConnector.setHost(Settings.getInstance().getBindAddress()); + portUnifiedConnector.setHost(Network.getInstance().getBindAddress()); portUnifiedConnector.setPort(Settings.getInstance().getGatewayPort()); this.server.addConnector(portUnifiedConnector); } else { // Non-SSL - InetAddress bindAddr = InetAddress.getByName(Settings.getInstance().getBindAddress()); + InetAddress bindAddr = InetAddress.getByName(Network.getInstance().getBindAddress()); InetSocketAddress endpoint = new InetSocketAddress(bindAddr, Settings.getInstance().getGatewayPort()); this.server = new Server(endpoint); } diff --git a/src/main/java/org/qortal/network/Network.java b/src/main/java/org/qortal/network/Network.java index f8f73c2a..ca79f367 100644 --- a/src/main/java/org/qortal/network/Network.java +++ b/src/main/java/org/qortal/network/Network.java @@ -124,6 +124,8 @@ public class Network { private final List selfPeers = new ArrayList<>(); + private String bindAddress = null; + private final ExecuteProduceConsume networkEPC; private Selector channelSelector; private ServerSocketChannel serverChannel; @@ -159,25 +161,43 @@ public class Network { // Grab P2P port from settings int listenPort = Settings.getInstance().getListenPort(); - // Grab P2P bind address from settings - try { - InetAddress bindAddr = InetAddress.getByName(Settings.getInstance().getBindAddress()); - InetSocketAddress endpoint = new InetSocketAddress(bindAddr, listenPort); + // Grab P2P bind addresses from settings + List bindAddresses = new ArrayList<>(); + if (Settings.getInstance().getBindAddress() != null) { + bindAddresses.add(Settings.getInstance().getBindAddress()); + } + if (Settings.getInstance().getBindAddressFallback() != null) { + bindAddresses.add(Settings.getInstance().getBindAddressFallback()); + } - channelSelector = Selector.open(); + for (int i=0; i