diff --git a/.classpath b/.classpath index 793e3dcc..95a79bf2 100644 --- a/.classpath +++ b/.classpath @@ -29,9 +29,9 @@ + - @@ -41,6 +41,7 @@ + diff --git a/pom.xml b/pom.xml index 8dd67a2f..31ceaaaa 100644 --- a/pom.xml +++ b/pom.xml @@ -425,6 +425,7 @@ ${swagger-ui.version} + org.bouncycastle diff --git a/src/main/java/org/qora/api/ApiService.java b/src/main/java/org/qora/api/ApiService.java index 05b12213..aed8977e 100644 --- a/src/main/java/org/qora/api/ApiService.java +++ b/src/main/java/org/qora/api/ApiService.java @@ -35,7 +35,7 @@ public class ApiService { // IP address based access control InetAccessHandler accessHandler = new InetAccessHandler(); - for (String pattern : Settings.getInstance().getApiAllowed()) { + for (String pattern : Settings.getInstance().getApiWhitelist()) { accessHandler.include(pattern); } this.server.setHandler(accessHandler); diff --git a/src/main/java/org/qora/api/resource/AddressesResource.java b/src/main/java/org/qora/api/resource/AddressesResource.java index ea68ae17..4b39b260 100644 --- a/src/main/java/org/qora/api/resource/AddressesResource.java +++ b/src/main/java/org/qora/api/resource/AddressesResource.java @@ -64,8 +64,8 @@ public class AddressesResource { if (accountData == null) accountData = new AccountData(address, null, null, BlockChain.getInstance().getDefaultGroupId()); - // If Blockchain config doesn't allow NO_GROUP then change this to blockchain's default groupID - if (accountData.getDefaultGroupId() == Group.NO_GROUP && !BlockChain.getInstance().getGrouplessAllowed()) + // If Blockchain config doesn't allow NO_GROUP for approval-needing tx type then change this to blockchain's default groupID + if (accountData.getDefaultGroupId() == Group.NO_GROUP && BlockChain.getInstance().getRequireGroupForApproval()) accountData.setDefaultGroupId(BlockChain.getInstance().getDefaultGroupId()); return accountData; @@ -242,7 +242,7 @@ public class AddressesResource { ) @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.NON_PRODUCTION, ApiError.REPOSITORY_ISSUE}) public String fromPublicKey(@PathParam("publickey") String publicKey58) { - if (Settings.getInstance().isRestrictedApi()) + if (Settings.getInstance().isApiRestricted()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); // Decode public key diff --git a/src/main/java/org/qora/api/resource/AssetsResource.java b/src/main/java/org/qora/api/resource/AssetsResource.java index a0e8b61c..7554d080 100644 --- a/src/main/java/org/qora/api/resource/AssetsResource.java +++ b/src/main/java/org/qora/api/resource/AssetsResource.java @@ -558,7 +558,7 @@ public class AssetsResource { ApiError.NON_PRODUCTION, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE, ApiError.TRANSACTION_INVALID }) public String cancelOrder(CancelAssetOrderTransactionData transactionData) { - if (Settings.getInstance().isRestrictedApi()) + if (Settings.getInstance().isApiRestricted()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); try (final Repository repository = RepositoryManager.getRepository()) { @@ -606,7 +606,7 @@ public class AssetsResource { ApiError.NON_PRODUCTION, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE, ApiError.TRANSACTION_INVALID }) public String issueAsset(IssueAssetTransactionData transactionData) { - if (Settings.getInstance().isRestrictedApi()) + if (Settings.getInstance().isApiRestricted()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); try (final Repository repository = RepositoryManager.getRepository()) { @@ -654,7 +654,7 @@ public class AssetsResource { ApiError.NON_PRODUCTION, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE, ApiError.TRANSACTION_INVALID }) public String createOrder(CreateAssetOrderTransactionData transactionData) { - if (Settings.getInstance().isRestrictedApi()) + if (Settings.getInstance().isApiRestricted()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); try (final Repository repository = RepositoryManager.getRepository()) { diff --git a/src/main/java/org/qora/api/resource/GroupsResource.java b/src/main/java/org/qora/api/resource/GroupsResource.java index 76723917..8e776319 100644 --- a/src/main/java/org/qora/api/resource/GroupsResource.java +++ b/src/main/java/org/qora/api/resource/GroupsResource.java @@ -267,7 +267,7 @@ public class GroupsResource { ) @ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) public String createGroup(CreateGroupTransactionData transactionData) { - if (Settings.getInstance().isRestrictedApi()) + if (Settings.getInstance().isApiRestricted()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); try (final Repository repository = RepositoryManager.getRepository()) { @@ -313,7 +313,7 @@ public class GroupsResource { ) @ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) public String updateGroup(UpdateGroupTransactionData transactionData) { - if (Settings.getInstance().isRestrictedApi()) + if (Settings.getInstance().isApiRestricted()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); try (final Repository repository = RepositoryManager.getRepository()) { @@ -359,7 +359,7 @@ public class GroupsResource { ) @ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) public String addGroupAdmin(AddGroupAdminTransactionData transactionData) { - if (Settings.getInstance().isRestrictedApi()) + if (Settings.getInstance().isApiRestricted()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); try (final Repository repository = RepositoryManager.getRepository()) { @@ -405,7 +405,7 @@ public class GroupsResource { ) @ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) public String removeGroupAdmin(RemoveGroupAdminTransactionData transactionData) { - if (Settings.getInstance().isRestrictedApi()) + if (Settings.getInstance().isApiRestricted()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); try (final Repository repository = RepositoryManager.getRepository()) { @@ -451,7 +451,7 @@ public class GroupsResource { ) @ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) public String groupBan(GroupBanTransactionData transactionData) { - if (Settings.getInstance().isRestrictedApi()) + if (Settings.getInstance().isApiRestricted()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); try (final Repository repository = RepositoryManager.getRepository()) { @@ -497,7 +497,7 @@ public class GroupsResource { ) @ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) public String cancelGroupBan(CancelGroupBanTransactionData transactionData) { - if (Settings.getInstance().isRestrictedApi()) + if (Settings.getInstance().isApiRestricted()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); try (final Repository repository = RepositoryManager.getRepository()) { @@ -543,7 +543,7 @@ public class GroupsResource { ) @ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) public String groupKick(GroupKickTransactionData transactionData) { - if (Settings.getInstance().isRestrictedApi()) + if (Settings.getInstance().isApiRestricted()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); try (final Repository repository = RepositoryManager.getRepository()) { @@ -589,7 +589,7 @@ public class GroupsResource { ) @ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) public String groupInvite(GroupInviteTransactionData transactionData) { - if (Settings.getInstance().isRestrictedApi()) + if (Settings.getInstance().isApiRestricted()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); try (final Repository repository = RepositoryManager.getRepository()) { @@ -635,7 +635,7 @@ public class GroupsResource { ) @ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) public String cancelGroupInvite(CancelGroupInviteTransactionData transactionData) { - if (Settings.getInstance().isRestrictedApi()) + if (Settings.getInstance().isApiRestricted()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); try (final Repository repository = RepositoryManager.getRepository()) { @@ -681,7 +681,7 @@ public class GroupsResource { ) @ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) public String joinGroup(JoinGroupTransactionData transactionData) { - if (Settings.getInstance().isRestrictedApi()) + if (Settings.getInstance().isApiRestricted()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); try (final Repository repository = RepositoryManager.getRepository()) { @@ -727,7 +727,7 @@ public class GroupsResource { ) @ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) public String leaveGroup(LeaveGroupTransactionData transactionData) { - if (Settings.getInstance().isRestrictedApi()) + if (Settings.getInstance().isApiRestricted()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); try (final Repository repository = RepositoryManager.getRepository()) { @@ -865,7 +865,7 @@ public class GroupsResource { ) @ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) public String groupApproval(GroupApprovalTransactionData transactionData) { - if (Settings.getInstance().isRestrictedApi()) + if (Settings.getInstance().isApiRestricted()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); try (final Repository repository = RepositoryManager.getRepository()) { @@ -911,7 +911,7 @@ public class GroupsResource { ) @ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) public String setGroup(SetGroupTransactionData transactionData) { - if (Settings.getInstance().isRestrictedApi()) + if (Settings.getInstance().isApiRestricted()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); try (final Repository repository = RepositoryManager.getRepository()) { diff --git a/src/main/java/org/qora/api/resource/NamesResource.java b/src/main/java/org/qora/api/resource/NamesResource.java index 318d9ccb..be93be10 100644 --- a/src/main/java/org/qora/api/resource/NamesResource.java +++ b/src/main/java/org/qora/api/resource/NamesResource.java @@ -161,7 +161,7 @@ public class NamesResource { ) @ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) public String registerName(RegisterNameTransactionData transactionData) { - if (Settings.getInstance().isRestrictedApi()) + if (Settings.getInstance().isApiRestricted()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); try (final Repository repository = RepositoryManager.getRepository()) { @@ -207,7 +207,7 @@ public class NamesResource { ) @ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) public String updateName(UpdateNameTransactionData transactionData) { - if (Settings.getInstance().isRestrictedApi()) + if (Settings.getInstance().isApiRestricted()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); try (final Repository repository = RepositoryManager.getRepository()) { @@ -253,7 +253,7 @@ public class NamesResource { ) @ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) public String sellName(SellNameTransactionData transactionData) { - if (Settings.getInstance().isRestrictedApi()) + if (Settings.getInstance().isApiRestricted()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); try (final Repository repository = RepositoryManager.getRepository()) { @@ -299,7 +299,7 @@ public class NamesResource { ) @ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) public String cancelSellName(CancelSellNameTransactionData transactionData) { - if (Settings.getInstance().isRestrictedApi()) + if (Settings.getInstance().isApiRestricted()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); try (final Repository repository = RepositoryManager.getRepository()) { @@ -345,7 +345,7 @@ public class NamesResource { ) @ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) public String buyName(BuyNameTransactionData transactionData) { - if (Settings.getInstance().isRestrictedApi()) + if (Settings.getInstance().isApiRestricted()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); try (final Repository repository = RepositoryManager.getRepository()) { diff --git a/src/main/java/org/qora/api/resource/PaymentsResource.java b/src/main/java/org/qora/api/resource/PaymentsResource.java index c2bc15e4..92047fe5 100644 --- a/src/main/java/org/qora/api/resource/PaymentsResource.java +++ b/src/main/java/org/qora/api/resource/PaymentsResource.java @@ -63,7 +63,7 @@ public class PaymentsResource { ) @ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) public String makePayment(PaymentTransactionData transactionData) { - if (Settings.getInstance().isRestrictedApi()) + if (Settings.getInstance().isApiRestricted()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); try (final Repository repository = RepositoryManager.getRepository()) { diff --git a/src/main/java/org/qora/api/resource/TransactionsResource.java b/src/main/java/org/qora/api/resource/TransactionsResource.java index 8b4ef832..8fcf131e 100644 --- a/src/main/java/org/qora/api/resource/TransactionsResource.java +++ b/src/main/java/org/qora/api/resource/TransactionsResource.java @@ -355,7 +355,7 @@ public class TransactionsResource { ApiError.NON_PRODUCTION, ApiError.INVALID_PRIVATE_KEY, ApiError.TRANSFORMATION_ERROR }) public String signTransaction(SimpleTransactionSignRequest signRequest) { - if (Settings.getInstance().isRestrictedApi()) + if (Settings.getInstance().isApiRestricted()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); if (signRequest.transactionBytes.length == 0) diff --git a/src/main/java/org/qora/api/resource/UtilsResource.java b/src/main/java/org/qora/api/resource/UtilsResource.java index a7a02f15..62252adf 100644 --- a/src/main/java/org/qora/api/resource/UtilsResource.java +++ b/src/main/java/org/qora/api/resource/UtilsResource.java @@ -79,7 +79,7 @@ public class UtilsResource { ) @ApiErrors({ApiError.NON_PRODUCTION, ApiError.INVALID_DATA}) public String fromBase64(String base64) { - if (Settings.getInstance().isRestrictedApi()) + if (Settings.getInstance().isApiRestricted()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); try { @@ -115,7 +115,7 @@ public class UtilsResource { ) @ApiErrors({ApiError.NON_PRODUCTION, ApiError.INVALID_DATA}) public String base64from58(String base58) { - if (Settings.getInstance().isRestrictedApi()) + if (Settings.getInstance().isApiRestricted()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); try { @@ -142,7 +142,7 @@ public class UtilsResource { ) @ApiErrors({ApiError.NON_PRODUCTION}) public String toBase64(@PathParam("hex") String hex) { - if (Settings.getInstance().isRestrictedApi()) + if (Settings.getInstance().isApiRestricted()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); return Base64.getEncoder().encodeToString(HashCode.fromString(hex).asBytes()); @@ -165,7 +165,7 @@ public class UtilsResource { ) @ApiErrors({ApiError.NON_PRODUCTION}) public String toBase58(@PathParam("hex") String hex) { - if (Settings.getInstance().isRestrictedApi()) + if (Settings.getInstance().isApiRestricted()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); return Base58.encode(HashCode.fromString(hex).asBytes()); @@ -190,7 +190,7 @@ public class UtilsResource { ) @ApiErrors({ApiError.NON_PRODUCTION}) public String random(@QueryParam("length") Integer length) { - if (Settings.getInstance().isRestrictedApi()) + if (Settings.getInstance().isApiRestricted()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); if (length == null) @@ -221,7 +221,7 @@ public class UtilsResource { ) @ApiErrors({ApiError.NON_PRODUCTION, ApiError.INVALID_DATA}) public String getMnemonic(@QueryParam("entropy") String suppliedEntropy) { - if (Settings.getInstance().isRestrictedApi()) + if (Settings.getInstance().isApiRestricted()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); /* @@ -290,7 +290,7 @@ public class UtilsResource { ) @ApiErrors({ApiError.NON_PRODUCTION}) public String fromMnemonic(String mnemonic) { - if (Settings.getInstance().isRestrictedApi()) + if (Settings.getInstance().isApiRestricted()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); if (mnemonic.isEmpty()) @@ -336,7 +336,7 @@ public class UtilsResource { ) @ApiErrors({ApiError.NON_PRODUCTION, ApiError.INVALID_DATA}) public String privateKey(@PathParam("entropy") String entropy58) { - if (Settings.getInstance().isRestrictedApi()) + if (Settings.getInstance().isApiRestricted()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); byte[] entropy; @@ -372,7 +372,7 @@ public class UtilsResource { ) @ApiErrors({ApiError.NON_PRODUCTION, ApiError.INVALID_DATA}) public String publicKey(@PathParam("privateKey") String privateKey58) { - if (Settings.getInstance().isRestrictedApi()) + if (Settings.getInstance().isApiRestricted()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); byte[] privateKey; diff --git a/src/main/java/org/qora/block/Block.java b/src/main/java/org/qora/block/Block.java index 012e932b..114cc75d 100644 --- a/src/main/java/org/qora/block/Block.java +++ b/src/main/java/org/qora/block/Block.java @@ -731,18 +731,21 @@ public class Block { if (this.blockData.getTimestamp() <= parentBlockData.getTimestamp()) return ValidationResult.TIMESTAMP_OLDER_THAN_PARENT; - // Check timestamp is not in the future (within configurable ~500ms margin) - if (this.blockData.getTimestamp() - BlockChain.getInstance().getBlockTimestampMargin() > NTP.getTime()) - return ValidationResult.TIMESTAMP_IN_FUTURE; + // These checks are disabled for testnet + if (!BlockChain.getInstance().isTestNet()) { + // Check timestamp is not in the future (within configurable ~500ms margin) + if (this.blockData.getTimestamp() - BlockChain.getInstance().getBlockTimestampMargin() > NTP.getTime()) + return ValidationResult.TIMESTAMP_IN_FUTURE; - // Legacy gen1 test: check timestamp milliseconds is the same as parent timestamp milliseconds? - if (this.blockData.getTimestamp() % 1000 != parentBlockData.getTimestamp() % 1000) - return ValidationResult.TIMESTAMP_MS_INCORRECT; + // Legacy gen1 test: check timestamp milliseconds is the same as parent timestamp milliseconds? + if (this.blockData.getTimestamp() % 1000 != parentBlockData.getTimestamp() % 1000) + return ValidationResult.TIMESTAMP_MS_INCORRECT; - // Too early to forge block? - // XXX DISABLED as it doesn't work - but why? - // if (this.blockData.getTimestamp() < parentBlock.getBlockData().getTimestamp() + BlockChain.getInstance().getMinBlockTime()) - // return ValidationResult.TIMESTAMP_TOO_SOON; + // Too early to forge block? + // XXX DISABLED as it doesn't work - but why? + // if (this.blockData.getTimestamp() < parentBlock.getBlockData().getTimestamp() + BlockChain.getInstance().getMinBlockTime()) + // return ValidationResult.TIMESTAMP_TOO_SOON; + } // Check block version if (this.blockData.getVersion() != parentBlock.getNextBlockVersion()) diff --git a/src/main/java/org/qora/block/BlockChain.java b/src/main/java/org/qora/block/BlockChain.java index 33e924f3..3d9c647f 100644 --- a/src/main/java/org/qora/block/BlockChain.java +++ b/src/main/java/org/qora/block/BlockChain.java @@ -1,17 +1,26 @@ package org.qora.block; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.io.Reader; import java.math.BigDecimal; import java.math.MathContext; import java.sql.SQLException; -import java.util.Collections; -import java.util.HashMap; import java.util.Map; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Unmarshaller; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import javax.xml.transform.stream.StreamSource; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; - -import org.json.simple.JSONObject; -import org.qora.data.asset.AssetData; +import org.eclipse.persistence.jaxb.JAXBContextFactory; +import org.eclipse.persistence.jaxb.UnmarshallerProperties; import org.qora.data.block.BlockData; import org.qora.group.Group; import org.qora.repository.BlockRepository; @@ -19,43 +28,59 @@ import org.qora.repository.DataException; import org.qora.repository.Repository; import org.qora.repository.RepositoryManager; import org.qora.settings.Settings; +import org.qora.utils.StringLongMapXmlAdapter; /** * Class representing the blockchain as a whole. * */ +// All properties to be converted to JSON via JAXB +@XmlAccessorType(XmlAccessType.FIELD) public class BlockChain { private static final Logger LOGGER = LogManager.getLogger(BlockChain.class); - public enum FeatureValueType { - height, - timestamp; - } - private static BlockChain instance = null; // Properties - private boolean isTestNet; + + private boolean isTestNet = false; + /** Maximum coin supply. */ + private BigDecimal maxBalance; + private BigDecimal unitFee; private BigDecimal maxBytesPerUnitFee; private BigDecimal minFeePerByte; - /** Maximum coin supply. */ - private BigDecimal maxBalance; + /** Number of blocks between recalculating block's generating balance. */ private int blockDifficultyInterval; - /** Minimum target time between blocks, in seconds. */ + /** Minimum target time between blocks, in milliseconds. */ private long minBlockTime; - /** Maximum target time between blocks, in seconds. */ + /** Maximum target time between blocks, in milliseconds. */ private long maxBlockTime; /** Maximum acceptable timestamp disagreement offset in milliseconds. */ private long blockTimestampMargin; + /** Whether transactions with txGroupId of NO_GROUP are allowed */ - private boolean grouplessAllowed; + private boolean requireGroupForApproval; /** Default groupID when account's default groupID isn't set */ private int defaultGroupId = Group.NO_GROUP; + + private GenesisBlock.GenesisInfo genesisInfo; + + public enum FeatureTrigger { + messageHeight, + atHeight, + assetsTimestamp, + votingTimestamp, + arbitraryTimestamp, + powfixTimestamp, + v2Timestamp; + } + /** Map of which blockchain features are enabled when (height/timestamp) */ - private Map> featureTriggers; + @XmlJavaTypeAdapter(StringLongMapXmlAdapter.class) + private Map featureTriggers; // This property is slightly different as we need it early and we want to avoid getInstance() loop private static boolean useBrokenMD160ForAddresses = false; @@ -73,9 +98,69 @@ public class BlockChain { return instance; } + public static void fileInstance(String filename) { + JAXBContext jc; + Unmarshaller unmarshaller; + + try { + // Create JAXB context aware of Settings + jc = JAXBContextFactory.createContext(new Class[] { + BlockChain.class, GenesisBlock.GenesisInfo.class + }, null); + + // Create unmarshaller + unmarshaller = jc.createUnmarshaller(); + + // Set the unmarshaller media type to JSON + unmarshaller.setProperty(UnmarshallerProperties.MEDIA_TYPE, "application/json"); + + // Tell unmarshaller that there's no JSON root element in the JSON input + unmarshaller.setProperty(UnmarshallerProperties.JSON_INCLUDE_ROOT, false); + + } catch (JAXBException e) { + LOGGER.error("Unable to process blockchain config file", e); + throw new RuntimeException("Unable to process blockchain config file", e); + } + + BlockChain blockchain = null; + + LOGGER.info("Using blockchain config file: " + filename); + + // Create the StreamSource by creating Reader to the JSON input + try (Reader settingsReader = new FileReader(filename)) { + StreamSource json = new StreamSource(settingsReader); + + // Attempt to unmarshal JSON stream to BlockChain config + blockchain = unmarshaller.unmarshal(json, BlockChain.class).getValue(); + } catch (FileNotFoundException e) { + LOGGER.error("Blockchain config file not found: " + filename); + throw new RuntimeException("Blockchain config file not found: " + filename); + } catch (JAXBException e) { + LOGGER.error("Unable to process blockchain config file", e); + throw new RuntimeException("Unable to process blockchain config file", e); + } catch (IOException e) { + LOGGER.error("Unable to process blockchain config file", e); + throw new RuntimeException("Unable to process blockchain config file", e); + } + + // Validate config + blockchain.validateConfig(); + + // Minor fix-up + blockchain.maxBytesPerUnitFee.setScale(8); + blockchain.unitFee.setScale(8); + blockchain.minFeePerByte = blockchain.unitFee.divide(blockchain.maxBytesPerUnitFee, MathContext.DECIMAL32); + + // Successfully read config now in effect + instance = blockchain; + + // Pass genesis info to GenesisBlock + GenesisBlock.newInstance(blockchain.genesisInfo); + } + // Getters / setters - public boolean getIsTestNet() { + public boolean isTestNet() { return this.isTestNet; } @@ -111,8 +196,9 @@ public class BlockChain { return this.blockTimestampMargin; } - public boolean getGrouplessAllowed() { - return this.grouplessAllowed; + /** Returns true if approval-needing transaction types require a txGroupId other than NO_GROUP. */ + public boolean getRequireGroupForApproval() { + return this.requireGroupForApproval; } public int getDefaultGroupId() { @@ -123,134 +209,62 @@ public class BlockChain { return useBrokenMD160ForAddresses; } - private long getFeatureTrigger(String feature, FeatureValueType valueType) { - Map featureTrigger = featureTriggers.get(feature); - if (featureTrigger == null) - return 0; - - Long value = featureTrigger.get(valueType); - if (value == null) - return 0; - - return value; - } - // Convenience methods for specific blockchain feature triggers public long getMessageReleaseHeight() { - return getFeatureTrigger("message", FeatureValueType.height); + return featureTriggers.get("messageHeight"); } public long getATReleaseHeight() { - return getFeatureTrigger("AT", FeatureValueType.height); + return featureTriggers.get("atHeight"); } public long getPowFixReleaseTimestamp() { - return getFeatureTrigger("powfix", FeatureValueType.timestamp); + return featureTriggers.get("powfixTimestamp"); } public long getAssetsReleaseTimestamp() { - return getFeatureTrigger("assets", FeatureValueType.timestamp); + return featureTriggers.get("assetsTimestamp"); } public long getVotingReleaseTimestamp() { - return getFeatureTrigger("voting", FeatureValueType.timestamp); + return featureTriggers.get("votingTimestamp"); } public long getArbitraryReleaseTimestamp() { - return getFeatureTrigger("arbitrary", FeatureValueType.timestamp); + return featureTriggers.get("arbitraryTimestamp"); } public long getQoraV2Timestamp() { - return getFeatureTrigger("v2", FeatureValueType.timestamp); + return featureTriggers.get("v2Timestamp"); } - // Blockchain config from JSON - - public static void fromJSON(JSONObject json) { - // Determine hash function for generating addresses as we need that to build genesis block, etc. - Boolean useBrokenMD160 = null; - if (json.containsKey("useBrokenMD160ForAddresses")) - useBrokenMD160 = (Boolean) Settings.getTypedJson(json, "useBrokenMD160ForAddresses", Boolean.class); - - if (useBrokenMD160 != null) - useBrokenMD160ForAddresses = useBrokenMD160.booleanValue(); - - Object genesisJson = json.get("genesis"); - if (genesisJson == null) { - LOGGER.error("No \"genesis\" entry found in blockchain config"); - throw new RuntimeException("No \"genesis\" entry found in blockchain config"); + /** Validate blockchain config read from JSON */ + private void validateConfig() { + if (this.genesisInfo == null) { + LOGGER.error("No \"genesisInfo\" entry found in blockchain config"); + throw new RuntimeException("No \"genesisInfo\" entry found in blockchain config"); } - GenesisBlock.fromJSON((JSONObject) genesisJson); - // Simple blockchain properties + if (this.featureTriggers == null) { + LOGGER.error("No \"featureTriggers\" entry found in blockchain config"); + throw new RuntimeException("No \"featureTriggers\" entry found in blockchain config"); + } - boolean grouplessAllowed = true; - if (json.containsKey("grouplessAllowed")) - grouplessAllowed = (Boolean) Settings.getTypedJson(json, "grouplessAllowed", Boolean.class); + // Check all featureTriggers are present + for (FeatureTrigger featureTrigger : FeatureTrigger.values()) + if (!this.featureTriggers.containsKey(featureTrigger.name())) { + LOGGER.error(String.format("Missing feature trigger \"%s\" in blockchain config", featureTrigger.name())); + throw new RuntimeException("Missing feature trigger in blockchain config"); + } - Integer defaultGroupId = null; - if (json.containsKey("defaultGroupId")) - defaultGroupId = ((Long) Settings.getTypedJson(json, "defaultGroupId", Long.class)).intValue(); - - // If groupless is not allowed the defaultGroupId needs to be set + // If groupless approval-needing transactions are not allowed the defaultGroupId needs to be set // XXX we could also check groupID exists, or at least created in genesis block, or in blockchain config - if (!grouplessAllowed && (defaultGroupId == null || defaultGroupId == Group.NO_GROUP)) { - LOGGER.error("defaultGroupId must be set to valid groupID in blockchain config if groupless transactions are not allowed"); - throw new RuntimeException("defaultGroupId must be set to valid groupID in blockchain config if groupless transactions are not allowed"); + if (!this.requireGroupForApproval && this.defaultGroupId == Group.NO_GROUP) { + LOGGER.error("defaultGroupId must be set to valid groupID in blockchain config if groupless approval-needing transactions are not allowed"); + throw new RuntimeException( + "defaultGroupId must be set to valid groupID in blockchain config if groupless approval-needing transactions are not allowed"); } - - boolean isTestNet = true; - if (json.containsKey("isTestNet")) - isTestNet = (Boolean) Settings.getTypedJson(json, "isTestNet", Boolean.class); - - BigDecimal unitFee = Settings.getJsonBigDecimal(json, "unitFee"); - long maxBytesPerUnitFee = (Long) Settings.getTypedJson(json, "maxBytesPerUnitFee", Long.class); - BigDecimal maxBalance = Settings.getJsonBigDecimal(json, "coinSupply"); - int blockDifficultyInterval = ((Long) Settings.getTypedJson(json, "blockDifficultyInterval", Long.class)).intValue(); - long minBlockTime = 1000L * (Long) Settings.getTypedJson(json, "minBlockTime", Long.class); // config entry in seconds - long maxBlockTime = 1000L * (Long) Settings.getTypedJson(json, "maxBlockTime", Long.class); // config entry in seconds - long blockTimestampMargin = (Long) Settings.getTypedJson(json, "blockTimestampMargin", Long.class); // config entry in milliseconds - - // blockchain feature triggers - Map> featureTriggers = new HashMap<>(); - JSONObject featuresJson = (JSONObject) Settings.getTypedJson(json, "featureTriggers", JSONObject.class); - for (Object feature : featuresJson.keySet()) { - String featureKey = (String) feature; - JSONObject trigger = (JSONObject) Settings.getTypedJson(featuresJson, featureKey, JSONObject.class); - - if (!trigger.containsKey("height") && !trigger.containsKey("timestamp")) { - LOGGER.error("Feature trigger \"" + featureKey + "\" must contain \"height\" or \"timestamp\" in blockchain config file"); - throw new RuntimeException("Feature trigger \"" + featureKey + "\" must contain \"height\" or \"timestamp\" in blockchain config file"); - } - - String triggerKey = (String) trigger.keySet().iterator().next(); - FeatureValueType featureValueType = FeatureValueType.valueOf(triggerKey); - if (featureValueType == null) { - LOGGER.error("Unrecognised feature trigger value type \"" + triggerKey + "\" for feature \"" + featureKey + "\" in blockchain config file"); - throw new RuntimeException( - "Unrecognised feature trigger value type \"" + triggerKey + "\" for feature \"" + featureKey + "\" in blockchain config file"); - } - - Long value = (Long) Settings.getJsonQuotedLong(trigger, triggerKey); - - featureTriggers.put(featureKey, Collections.singletonMap(featureValueType, value)); - } - - instance = new BlockChain(); - instance.isTestNet = isTestNet; - instance.unitFee = unitFee; - instance.maxBytesPerUnitFee = BigDecimal.valueOf(maxBytesPerUnitFee).setScale(8); - instance.minFeePerByte = unitFee.divide(instance.maxBytesPerUnitFee, MathContext.DECIMAL32); - instance.maxBalance = maxBalance; - instance.blockDifficultyInterval = blockDifficultyInterval; - instance.minBlockTime = minBlockTime; - instance.maxBlockTime = maxBlockTime; - instance.blockTimestampMargin = blockTimestampMargin; - instance.grouplessAllowed = grouplessAllowed; - if (defaultGroupId != null) - instance.defaultGroupId = defaultGroupId; - instance.featureTriggers = featureTriggers; } /** @@ -287,11 +301,6 @@ public class BlockChain { GenesisBlock genesisBlock = GenesisBlock.getInstance(repository); - // Add initial assets - // NOTE: Asset's [transaction] reference doesn't exist as a transaction! - for (AssetData assetData : genesisBlock.getInitialAssets()) - repository.getAssetRepository().save(assetData); - // Add Genesis Block to blockchain genesisBlock.process(); diff --git a/src/main/java/org/qora/block/BlockGenerator.java b/src/main/java/org/qora/block/BlockGenerator.java index a8c04a1b..93add299 100644 --- a/src/main/java/org/qora/block/BlockGenerator.java +++ b/src/main/java/org/qora/block/BlockGenerator.java @@ -28,8 +28,6 @@ public class BlockGenerator extends Thread { // Properties private byte[] generatorPrivateKey; private PrivateKeyAccount generator; - private Block previousBlock; - private Block newBlock; private boolean running; // Other properties @@ -39,8 +37,6 @@ public class BlockGenerator extends Thread { public BlockGenerator(byte[] generatorPrivateKey) { this.generatorPrivateKey = generatorPrivateKey; - this.previousBlock = null; - this.newBlock = null; this.running = true; } @@ -66,6 +62,8 @@ public class BlockGenerator extends Thread { // Going to need this a lot... BlockRepository blockRepository = repository.getBlockRepository(); + Block previousBlock = null; + Block newBlock = null; while (running) { // Check blockchain hasn't changed @@ -82,39 +80,41 @@ public class BlockGenerator extends Thread { // Make sure we're the only thread modifying the blockchain Lock blockchainLock = Controller.getInstance().getBlockchainLock(); if (blockchainLock.tryLock()) - try { + generation: try { // Is new block valid yet? (Before adding unconfirmed transactions) - if (newBlock.isValid() == ValidationResult.OK) { - // Delete invalid transactions - deleteInvalidTransactions(repository); + if (newBlock.isValid() != ValidationResult.OK) + break generation; - // Add unconfirmed transactions - addUnconfirmedTransactions(repository, newBlock); + // Delete invalid transactions + deleteInvalidTransactions(repository); - // Sign to create block's signature - newBlock.sign(); + // Add unconfirmed transactions + addUnconfirmedTransactions(repository, newBlock); - // If newBlock is still valid then we can use it - ValidationResult validationResult = newBlock.isValid(); - if (validationResult == ValidationResult.OK) { - // Add to blockchain - something else will notice and broadcast new block to network - try { - newBlock.process(); - LOGGER.info("Generated new block: " + newBlock.getBlockData().getHeight()); - repository.saveChanges(); + // Sign to create block's signature + newBlock.sign(); - // Notify controller - Controller.getInstance().onGeneratedBlock(newBlock.getBlockData()); - } catch (DataException e) { - // Unable to process block - report and discard - LOGGER.error("Unable to process newly generated block?", e); - newBlock = null; - } - } else { - // No longer valid? Report and discard - LOGGER.error("Valid, generated block now invalid '" + validationResult.name() + "' after adding unconfirmed transactions?"); - newBlock = null; - } + // Is newBlock still valid? + ValidationResult validationResult = newBlock.isValid(); + if (validationResult != ValidationResult.OK) { + // No longer valid? Report and discard + LOGGER.error("Valid, generated block now invalid '" + validationResult.name() + "' after adding unconfirmed transactions?"); + newBlock = null; + break generation; + } + + // Add to blockchain - something else will notice and broadcast new block to network + try { + newBlock.process(); + LOGGER.info("Generated new block: " + newBlock.getBlockData().getHeight()); + repository.saveChanges(); + + // Notify controller + Controller.getInstance().onGeneratedBlock(newBlock.getBlockData()); + } catch (DataException e) { + // Unable to process block - report and discard + LOGGER.error("Unable to process newly generated block?", e); + newBlock = null; } } finally { blockchainLock.unlock(); @@ -134,7 +134,7 @@ public class BlockGenerator extends Thread { } } - private void deleteInvalidTransactions(Repository repository) throws DataException { + private static void deleteInvalidTransactions(Repository repository) throws DataException { List invalidTransactions = Transaction.getInvalidTransactions(repository); // Actually delete invalid transactions from database @@ -145,7 +145,7 @@ public class BlockGenerator extends Thread { repository.saveChanges(); } - private void addUnconfirmedTransactions(Repository repository, Block newBlock) throws DataException { + private static void addUnconfirmedTransactions(Repository repository, Block newBlock) throws DataException { // Grab all valid unconfirmed transactions (already sorted) List unconfirmedTransactions = Transaction.getUnconfirmedTransactions(repository); @@ -200,4 +200,41 @@ public class BlockGenerator extends Thread { this.interrupt(); } + public static void generateTestingBlock(Repository repository, PrivateKeyAccount generator) throws DataException { + if (!BlockChain.getInstance().isTestNet()) { + LOGGER.warn("Attempt to generating testing block but not in testnet mode!"); + return; + } + + BlockData previousBlockData = repository.getBlockRepository().getLastBlock(); + + Block newBlock = new Block(repository, previousBlockData, generator); + + // Make sure we're the only thread modifying the blockchain + Lock blockchainLock = Controller.getInstance().getBlockchainLock(); + if (blockchainLock.tryLock()) + try { + // Delete invalid transactions + deleteInvalidTransactions(repository); + + // Add unconfirmed transactions + addUnconfirmedTransactions(repository, newBlock); + + // Sign to create block's signature + newBlock.sign(); + + // Is newBlock still valid? + ValidationResult validationResult = newBlock.isValid(); + if (validationResult != ValidationResult.OK) + throw new IllegalStateException( + "Valid, generated block now invalid '" + validationResult.name() + "' after adding unconfirmed transactions?"); + + // Add to blockchain + newBlock.process(); + repository.saveChanges(); + } finally { + blockchainLock.unlock(); + } + } + } diff --git a/src/main/java/org/qora/block/GenesisBlock.java b/src/main/java/org/qora/block/GenesisBlock.java index 8cff5d8c..c3cc1af8 100644 --- a/src/main/java/org/qora/block/GenesisBlock.java +++ b/src/main/java/org/qora/block/GenesisBlock.java @@ -3,26 +3,30 @@ package org.qora.block; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.math.BigDecimal; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.bitcoinj.core.Base58; -import org.json.simple.JSONArray; -import org.json.simple.JSONObject; +import org.qora.account.Account; import org.qora.account.GenesisAccount; +import org.qora.account.PublicKeyAccount; import org.qora.crypto.Crypto; import org.qora.data.asset.AssetData; import org.qora.data.block.BlockData; -import org.qora.data.transaction.GenesisTransactionData; +import org.qora.data.transaction.IssueAssetTransactionData; import org.qora.data.transaction.TransactionData; import org.qora.repository.DataException; import org.qora.repository.Repository; -import org.qora.settings.Settings; import org.qora.transaction.Transaction; +import org.qora.transaction.Transaction.TransactionType; +import org.qora.transform.TransformationException; +import org.qora.transform.transaction.TransactionTransformer; import com.google.common.primitives.Bytes; import com.google.common.primitives.Longs; @@ -34,7 +38,18 @@ public class GenesisBlock extends Block { private static final byte[] GENESIS_REFERENCE = new byte[] { 1, 1, 1, 1, 1, 1, 1, 1 }; // NOTE: Neither 64 nor 128 bytes! - private static final byte[] GENESIS_GENERATOR_PUBLIC_KEY = GenesisAccount.PUBLIC_KEY; // NOTE: 8 bytes not 32 bytes! + + @XmlAccessorType(XmlAccessType.FIELD) + public static class GenesisInfo { + public int version = 1; + public long timestamp; + public BigDecimal generatingBalance; + + public TransactionData[] transactions; + + public GenesisInfo() { + } + } // Properties private static BlockData blockData; @@ -53,95 +68,74 @@ public class GenesisBlock extends Block { // Construction from JSON - public static void fromJSON(JSONObject json) { - // All parsing first, then if successful we can proceed to construction + /** Construct block data from blockchain config */ + public static void newInstance(GenesisInfo info) { + // Should be safe to make this call as BlockChain's instance is set + // so we won't be blocked trying to re-enter synchronzied Settings.getInstance() + BlockChain blockchain = BlockChain.getInstance(); - // Version - int version = 1; // but could be bumped later - - // Timestamp - String timestampStr = (String) Settings.getTypedJson(json, "timestamp", String.class); - long timestamp; - - if (timestampStr.equals("now")) - timestamp = System.currentTimeMillis(); - else - try { - timestamp = Long.parseUnsignedLong(timestampStr); - } catch (NumberFormatException e) { - LOGGER.error("Unable to parse genesis timestamp: " + timestampStr); - throw new RuntimeException("Unable to parse genesis timestamp"); + // Timestamp of zero means "now" but only valid for test nets! + if (info.timestamp == 0) { + if (!blockchain.isTestNet()) { + LOGGER.error("Genesis timestamp of zero (i.e. now) not valid for non-testnet blockchain configs"); + throw new RuntimeException("Genesis timestamp of zero (i.e. now) not valid for non-testnet blockchain configs"); } - // Generating balance - BigDecimal generatingBalance = Settings.getJsonBigDecimal(json, "generatingBalance"); - - // Transactions - JSONArray transactionsJson = (JSONArray) Settings.getTypedJson(json, "transactions", JSONArray.class); - List transactions = new ArrayList<>(); - - for (Object transactionObj : transactionsJson) { - if (!(transactionObj instanceof JSONObject)) { - LOGGER.error("Genesis transaction malformed in blockchain config file"); - throw new RuntimeException("Genesis transaction malformed in blockchain config file"); - } - - JSONObject transactionJson = (JSONObject) transactionObj; - - String recipient = (String) Settings.getTypedJson(transactionJson, "recipient", String.class); - BigDecimal amount = Settings.getJsonBigDecimal(transactionJson, "amount"); - - // assetId is optional - if (transactionJson.containsKey("assetId")) { - long assetId = (Long) Settings.getTypedJson(transactionJson, "assetId", Long.class); - - // We're into version 4 genesis block territory now - version = 4; - - transactions.add(new GenesisTransactionData(timestamp, recipient, amount, assetId)); - } else { - transactions.add(new GenesisTransactionData(timestamp, recipient, amount)); - } + // This will only take effect if there is no current genesis block in blockchain + info.timestamp = System.currentTimeMillis(); } - // Assets - JSONArray assetsJson = (JSONArray) Settings.getTypedJson(json, "assets", JSONArray.class); - String genesisAddress = Crypto.toAddress(GenesisAccount.PUBLIC_KEY); - List assets = new ArrayList<>(); + transactionsData = Arrays.asList(info.transactions); - for (Object assetObj : assetsJson) { - if (!(assetObj instanceof JSONObject)) { - LOGGER.error("Genesis asset malformed in blockchain config file"); - throw new RuntimeException("Genesis asset malformed in blockchain config file"); + // Add default values to transactions + transactionsData.stream().forEach(transactionData -> { + if (transactionData.getFee() == null) + transactionData.setFee(BigDecimal.ZERO.setScale(8)); + + if (transactionData.getCreatorPublicKey() == null) + transactionData.setCreatorPublicKey(GenesisAccount.PUBLIC_KEY); + + if (transactionData.getTimestamp() == 0) + transactionData.setTimestamp(info.timestamp); + }); + + // For version 1, extract any ISSUE_ASSET transactions into initialAssets and only allow GENESIS transactions + if (info.version == 1) { + List issueAssetTransactions = transactionsData.stream() + .filter(transactionData -> transactionData.getType() == TransactionType.ISSUE_ASSET).collect(Collectors.toList()); + transactionsData.removeAll(issueAssetTransactions); + + // There should be only GENESIS transactions left; + if (transactionsData.stream().anyMatch(transactionData -> transactionData.getType() != TransactionType.GENESIS)) { + LOGGER.error("Version 1 genesis block only allowed to contain GENESIS transctions (after issue-asset processing)"); + throw new RuntimeException("Version 1 genesis block only allowed to contain GENESIS transctions (after issue-asset processing)"); } - JSONObject assetJson = (JSONObject) assetObj; + // Convert ISSUE_ASSET transactions into initial assets + issueAssetTransactions.stream().map(transactionData -> { + IssueAssetTransactionData issueAssetTransactionData = (IssueAssetTransactionData) transactionData; - String name = (String) Settings.getTypedJson(assetJson, "name", String.class); - String description = (String) Settings.getTypedJson(assetJson, "description", String.class); - String reference58 = (String) Settings.getTypedJson(assetJson, "reference", String.class); - byte[] reference = Base58.decode(reference58); - long quantity = (Long) Settings.getTypedJson(assetJson, "quantity", Long.class); - boolean isDivisible = (Boolean) Settings.getTypedJson(assetJson, "isDivisible", Boolean.class); - - assets.add(new AssetData(genesisAddress, name, description, quantity, isDivisible, reference)); + return new AssetData(issueAssetTransactionData.getOwner(), issueAssetTransactionData.getAssetName(), issueAssetTransactionData.getDescription(), + issueAssetTransactionData.getQuantity(), issueAssetTransactionData.getIsDivisible(), issueAssetTransactionData.getReference()); + }).collect(Collectors.toList()); } + // Minor fix-up + info.generatingBalance.setScale(8); + byte[] reference = GENESIS_REFERENCE; - int transactionCount = transactions.size(); + int transactionCount = transactionsData.size(); BigDecimal totalFees = BigDecimal.ZERO.setScale(8); - byte[] generatorPublicKey = GENESIS_GENERATOR_PUBLIC_KEY; - byte[] bytesForSignature = getBytesForSignature(version, reference, generatingBalance, generatorPublicKey); + byte[] generatorPublicKey = GenesisAccount.PUBLIC_KEY; + byte[] bytesForSignature = getBytesForSignature(info.version, reference, info.generatingBalance, generatorPublicKey); byte[] generatorSignature = calcSignature(bytesForSignature); byte[] transactionsSignature = generatorSignature; int height = 1; int atCount = 0; BigDecimal atFees = BigDecimal.ZERO.setScale(8); - blockData = new BlockData(version, reference, transactionCount, totalFees, transactionsSignature, height, timestamp, generatingBalance, + blockData = new BlockData(info.version, reference, transactionCount, totalFees, transactionsSignature, height, info.timestamp, info.generatingBalance, generatorPublicKey, generatorSignature, atCount, atFees); - transactionsData = transactions; - initialAssets = assets; } // More information @@ -283,4 +277,45 @@ public class GenesisBlock extends Block { return ValidationResult.OK; } + @Override + public void process() throws DataException { + LOGGER.info(String.format("Using genesis block timestamp of %d", blockData.getTimestamp())); + + // If we're a version 1 genesis block, create assets now + if (blockData.getVersion() == 1) + for (AssetData assetData : initialAssets) + repository.getAssetRepository().save(assetData); + + /* + * Some transactions will be missing references and signatures, + * so we generate them by trial-processing transactions and using + * account's last-reference to fill in the gaps for reference, + * and a duplicated SHA256 digest for signature + */ + this.repository.setSavepoint(); + try { + for (Transaction transaction : this.getTransactions()) { + TransactionData transactionData = transaction.getTransactionData(); + Account creator = new PublicKeyAccount(this.repository, transactionData.getCreatorPublicKey()); + + if (transactionData.getReference() == null) + transactionData.setReference(creator.getLastReference()); + if (transactionData.getSignature() == null) { + byte[] digest = Crypto.digest(TransactionTransformer.toBytesForSigning(transactionData)); + byte[] signature = Bytes.concat(digest, digest); + + transactionData.setSignature(signature); + } + + transaction.process(); + } + } catch (TransformationException e) { + throw new RuntimeException("Can't process genesis block transaction", e); + } finally { + this.repository.rollbackToSavepoint(); + } + + super.process(); + } + } diff --git a/src/main/java/org/qora/data/PaymentData.java b/src/main/java/org/qora/data/PaymentData.java index 41406573..36a51476 100644 --- a/src/main/java/org/qora/data/PaymentData.java +++ b/src/main/java/org/qora/data/PaymentData.java @@ -5,7 +5,7 @@ import java.math.BigDecimal; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; -// All properties to be converted to JSON via JAX-RS +// All properties to be converted to JSON via JAXB @XmlAccessorType(XmlAccessType.FIELD) public class PaymentData { @@ -16,7 +16,7 @@ public class PaymentData { // Constructors - // For JAX-RS + // For JAXB protected PaymentData() { } diff --git a/src/main/java/org/qora/data/transaction/CreateGroupTransactionData.java b/src/main/java/org/qora/data/transaction/CreateGroupTransactionData.java index 06fd4e27..f7980236 100644 --- a/src/main/java/org/qora/data/transaction/CreateGroupTransactionData.java +++ b/src/main/java/org/qora/data/transaction/CreateGroupTransactionData.java @@ -6,6 +6,7 @@ import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; +import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorValue; import org.qora.group.Group.ApprovalThreshold; import org.qora.transaction.Transaction.TransactionType; @@ -19,6 +20,8 @@ import io.swagger.v3.oas.annotations.media.Schema.AccessMode; TransactionData.class } ) +//JAXB: use this subclass if XmlDiscriminatorNode matches XmlDiscriminatorValue below: +@XmlDiscriminatorValue("CREATE_GROUP") public class CreateGroupTransactionData extends TransactionData { // Properties diff --git a/src/main/java/org/qora/data/transaction/GenesisTransactionData.java b/src/main/java/org/qora/data/transaction/GenesisTransactionData.java index 3a1725a0..63df40fa 100644 --- a/src/main/java/org/qora/data/transaction/GenesisTransactionData.java +++ b/src/main/java/org/qora/data/transaction/GenesisTransactionData.java @@ -5,6 +5,7 @@ import java.math.BigDecimal; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; +import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorValue; import org.qora.account.GenesisAccount; import org.qora.asset.Asset; import org.qora.transaction.Transaction.TransactionType; @@ -18,6 +19,8 @@ import io.swagger.v3.oas.annotations.media.Schema; TransactionData.class } ) +//JAXB: use this subclass if XmlDiscriminatorNode matches XmlDiscriminatorValue below: +@XmlDiscriminatorValue("GENESIS") public class GenesisTransactionData extends TransactionData { // Properties diff --git a/src/main/java/org/qora/data/transaction/IssueAssetTransactionData.java b/src/main/java/org/qora/data/transaction/IssueAssetTransactionData.java index 7e57a665..d101d3f0 100644 --- a/src/main/java/org/qora/data/transaction/IssueAssetTransactionData.java +++ b/src/main/java/org/qora/data/transaction/IssueAssetTransactionData.java @@ -6,6 +6,9 @@ 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.qora.account.GenesisAccount; +import org.qora.block.GenesisBlock; import org.qora.transaction.Transaction.TransactionType; import io.swagger.v3.oas.annotations.media.Schema; @@ -14,6 +17,8 @@ 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 }) +// JAXB: use this subclass if XmlDiscriminatorNode matches XmlDiscriminatorValue below: +@XmlDiscriminatorValue("ISSUE_ASSET") public class IssueAssetTransactionData extends TransactionData { // Properties @@ -41,6 +46,9 @@ public class IssueAssetTransactionData extends TransactionData { } public void afterUnmarshal(Unmarshaller u, Object parent) { + if (parent instanceof GenesisBlock.GenesisInfo && this.issuerPublicKey == null) + this.issuerPublicKey = GenesisAccount.PUBLIC_KEY; + this.creatorPublicKey = this.issuerPublicKey; } diff --git a/src/main/java/org/qora/data/transaction/TransactionData.java b/src/main/java/org/qora/data/transaction/TransactionData.java index 4869ce38..02e90f99 100644 --- a/src/main/java/org/qora/data/transaction/TransactionData.java +++ b/src/main/java/org/qora/data/transaction/TransactionData.java @@ -10,6 +10,7 @@ import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlSeeAlso; import javax.xml.bind.annotation.XmlTransient; +import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorNode; import org.qora.crypto.Crypto; import org.qora.transaction.Transaction.TransactionType; @@ -39,6 +40,8 @@ import io.swagger.v3.oas.annotations.media.Schema.AccessMode; }) //All properties to be converted to JSON via JAXB @XmlAccessorType(XmlAccessType.FIELD) +// EclipseLink JAXB (MOXy) specific: use "type" field to determine subclass +@XmlDiscriminatorNode("type") public abstract class TransactionData { // Properties shared with all transaction types @@ -97,6 +100,10 @@ public abstract class TransactionData { return this.timestamp; } + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + public int getTxGroupId() { return this.txGroupId; } @@ -105,6 +112,10 @@ public abstract class TransactionData { return this.reference; } + public void setReference(byte[] reference) { + this.reference = reference; + } + public byte[] getCreatorPublicKey() { return this.creatorPublicKey; } @@ -118,6 +129,10 @@ public abstract class TransactionData { return this.fee; } + public void setFee(BigDecimal fee) { + this.fee = fee; + } + public byte[] getSignature() { return this.signature; } diff --git a/src/main/java/org/qora/settings/Settings.java b/src/main/java/org/qora/settings/Settings.java index 3e2919eb..b0561923 100644 --- a/src/main/java/org/qora/settings/Settings.java +++ b/src/main/java/org/qora/settings/Settings.java @@ -1,233 +1,197 @@ package org.qora.settings; import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; import java.io.IOException; -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; +import java.io.Reader; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Unmarshaller; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.transform.stream.StreamSource; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; - -import org.json.simple.JSONArray; -import org.json.simple.JSONObject; -import org.json.simple.JSONValue; +import org.eclipse.persistence.jaxb.JAXBContextFactory; +import org.eclipse.persistence.jaxb.UnmarshallerProperties; import org.qora.block.BlockChain; -import com.google.common.base.Charsets; -import com.google.common.io.Files; - +// All properties to be converted to JSON via JAXB +@XmlAccessorType(XmlAccessType.FIELD) public class Settings { + public static final int DEFAULT_LISTEN_PORT = 9084; + private static final Logger LOGGER = LogManager.getLogger(Settings.class); + private static final String SETTINGS_FILENAME = "settings.json"; // Properties private static Settings instance; - private String userpath = ""; - private boolean useBitcoinTestNet = false; + + // Settings, and other config files + private String userPath; + + // API-related + private boolean apiEnabled = true; + private int apiPort = 9085; + private String[] apiWhitelist = new String[] { + "::1", "127.0.0.1" + }; + private Boolean apiRestricted; + + // Specific to this node private boolean wipeUnconfirmedOnStart = false; - private Boolean restrictedApi; - private String blockchainConfigPath = "blockchain.json"; /** Maximum number of unconfirmed transactions allowed per account */ private int maxUnconfirmedPerAccount = 100; /** Max milliseconds into future for accepting new, unconfirmed transactions */ - private long maxTransactionTimestampFuture = 24 * 60 * 60 * 1000; // milliseconds + private int maxTransactionTimestampFuture = 24 * 60 * 60 * 1000; // milliseconds - // API - private int apiPort = 9085; - private List apiAllowed = new ArrayList(Arrays.asList("127.0.0.1", "::1")); // ipv4, ipv6 - private boolean apiEnabled = true; - - // Peer-to-peer networking - public static final int DEFAULT_LISTEN_PORT = 9084; + // Peer-to-peer related private int listenPort = DEFAULT_LISTEN_PORT; private String bindAddress = null; // listen on all local addresses private int minPeers = 3; private int maxPeers = 10; - // Constants - private static final String SETTINGS_FILENAME = "settings.json"; + // Which blockchains this node is running + private String blockchainConfig = "blockchain.json"; + private boolean useBitcoinTestNet = false; // Constructors private Settings() { } - private Settings(String filename) { - // Read from file - String path = ""; - - try { - do { - File file = new File(path + filename); - - if (!file.exists()) { - // log lack of settings file - LOGGER.info("Settings file not found: " + path + filename); - break; - } - - LOGGER.info("Using settings file: " + path + filename); - List lines = Files.readLines(file, Charsets.UTF_8); - - // Concatenate lines for JSON parsing - String jsonString = ""; - for (String line : lines) { - // Escape single backslashes in "userpath" entries, typically Windows-style paths - if (line.contains("userpath")) - line.replace("\\", "\\\\"); - - jsonString += line; - } - - JSONObject settingsJSON = (JSONObject) JSONValue.parse(jsonString); - - String userpath = (String) settingsJSON.get("userpath"); - if (userpath != null) { - path = userpath; - - // Add trailing directory separator if needed - if (!path.endsWith(File.separator)) - path += File.separator; - - continue; - } - - this.userpath = path; - process(settingsJSON); - - break; - } while (true); - } catch (IOException | ClassCastException e) { - LOGGER.error("Unable to parse settings file: " + path + filename); - throw new RuntimeException("Unable to parse settings file", e); - } - } - // Other methods public static synchronized Settings getInstance() { if (instance == null) - instance = new Settings(SETTINGS_FILENAME); + fileInstance(SETTINGS_FILENAME); return instance; } - public static void test(JSONObject settingsJSON) { - // Discard previous settings - if (instance != null) - instance = null; - - instance = new Settings(); - getInstance().process(settingsJSON); - } - - private void process(JSONObject json) { - // API - if (json.containsKey("apiPort")) - this.apiPort = ((Long) json.get("apiPort")).intValue(); - - if (json.containsKey("apiAllowed")) { - JSONArray allowedArray = (JSONArray) json.get("apiAllowed"); - - this.apiAllowed = new ArrayList(); - - for (Object entry : allowedArray) { - if (!(entry instanceof String)) - throw new RuntimeException("Entry inside 'apiAllowed' is not string"); - - this.apiAllowed.add((String) entry); - } - } - - if (json.containsKey("apiEnabled")) - this.apiEnabled = ((Boolean) json.get("apiEnabled")).booleanValue(); - - if (json.containsKey("restrictedApi")) - this.restrictedApi = ((Boolean) json.get("restrictedApi")).booleanValue(); - - // Peer-to-peer networking - - if (json.containsKey("listenPort")) - this.listenPort = ((Long) getTypedJson(json, "listenPort", Long.class)).intValue(); - - if (json.containsKey("bindAddress")) - this.bindAddress = (String) getTypedJson(json, "bindAddress", String.class); - - if (json.containsKey("minPeers")) - this.minPeers = ((Long) getTypedJson(json, "minPeers", Long.class)).intValue(); - - if (json.containsKey("maxPeers")) - this.maxPeers = ((Long) getTypedJson(json, "maxPeers", Long.class)).intValue(); - - // Node-specific behaviour - - if (json.containsKey("wipeUnconfirmedOnStart")) - this.wipeUnconfirmedOnStart = (Boolean) getTypedJson(json, "wipeUnconfirmedOnStart", Boolean.class); - - if (json.containsKey("maxUnconfirmedPerAccount")) - this.maxUnconfirmedPerAccount = ((Long) getTypedJson(json, "maxUnconfirmedPerAccount", Long.class)).intValue(); - - if (json.containsKey("maxTransactionTimestampFuture")) - this.maxTransactionTimestampFuture = (Long) getTypedJson(json, "maxTransactionTimestampFuture", Long.class); - - // Blockchain config - - if (json.containsKey("blockchainConfig")) - blockchainConfigPath = (String) getTypedJson(json, "blockchainConfig", String.class); - - File file = new File(this.userpath + blockchainConfigPath); - - if (!file.exists()) { - LOGGER.info("Blockchain config file not found: " + this.userpath + blockchainConfigPath); - throw new RuntimeException("Unable to read blockchain config file"); - } + public static void fileInstance(String filename) { + JAXBContext jc; + Unmarshaller unmarshaller; try { - List lines = Files.readLines(file, Charsets.UTF_8); - JSONObject blockchainJSON = (JSONObject) JSONValue.parse(String.join("\n", lines)); - BlockChain.fromJSON(blockchainJSON); - } catch (IOException e) { - LOGGER.error("Unable to parse blockchain config file: " + this.userpath + blockchainConfigPath); - throw new RuntimeException("Unable to parse blockchain config file", e); + // Create JAXB context aware of Settings + jc = JAXBContextFactory.createContext(new Class[] { + Settings.class + }, null); + + // Create unmarshaller + unmarshaller = jc.createUnmarshaller(); + + // Set the unmarshaller media type to JSON + unmarshaller.setProperty(UnmarshallerProperties.MEDIA_TYPE, "application/json"); + + // Tell unmarshaller that there's no JSON root element in the JSON input + unmarshaller.setProperty(UnmarshallerProperties.JSON_INCLUDE_ROOT, false); + + } catch (JAXBException e) { + LOGGER.error("Unable to process settings file", e); + throw new RuntimeException("Unable to process settings file", e); } + + Settings settings = null; + String path = ""; + + do { + LOGGER.info("Using settings file: " + path + filename); + + // Create the StreamSource by creating Reader to the JSON input + try (Reader settingsReader = new FileReader(filename)) { + StreamSource json = new StreamSource(settingsReader); + + // Attempt to unmarshal JSON stream to Settings + settings = unmarshaller.unmarshal(json, Settings.class).getValue(); + } catch (FileNotFoundException e) { + LOGGER.error("Settings file not found: " + path + filename); + throw new RuntimeException("Settings file not found: " + path + filename); + } catch (JAXBException e) { + LOGGER.error("Unable to process settings file", e); + throw new RuntimeException("Unable to process settings file", e); + } catch (IOException e) { + LOGGER.error("Unable to process settings file", e); + throw new RuntimeException("Unable to process settings file", e); + } + + if (settings.userPath != null) { + // Adjust filename and go round again + path = settings.userPath; + + // Add trailing directory separator if needed + if (!path.endsWith(File.separator)) + path += File.separator; + + } + } while (settings.userPath != null); + + // Validate settings + settings.validate(); + + // Minor fix-up + if (settings.userPath == null) + settings.userPath = ""; + + // Successfully read settings now in effect + instance = settings; + + // Now read blockchain config + BlockChain.fileInstance(settings.getUserPath() + settings.getBlockchainConfig()); + } + + private void validate() { + // Validation goes here } // Getters / setters - public String getUserpath() { - return this.userpath; - } - - public int getApiPort() { - return this.apiPort; - } - - public List getApiAllowed() { - return this.apiAllowed; + public String getUserPath() { + return this.userPath; } public boolean isApiEnabled() { return this.apiEnabled; } - public boolean isRestrictedApi() { - if (this.restrictedApi != null) - return this.restrictedApi; + public int getApiPort() { + return this.apiPort; + } + + public String[] getApiWhitelist() { + return this.apiWhitelist; + } + + public boolean isApiRestricted() { + // Explicitly set value takes precedence + if (this.apiRestricted != null) + return this.apiRestricted; // Not set in config file, so restrict if not testnet - return !BlockChain.getInstance().getIsTestNet(); + return !BlockChain.getInstance().isTestNet(); + } + + public boolean getWipeUnconfirmedOnStart() { + return this.wipeUnconfirmedOnStart; + } + + public int getMaxUnconfirmedPerAccount() { + return this.maxUnconfirmedPerAccount; + } + + public int getMaxTransactionTimestampFuture() { + return this.maxTransactionTimestampFuture; } public int getListenPort() { return this.listenPort; } - public int getDefaultListenPort() { - return DEFAULT_LISTEN_PORT; - } - public String getBindAddress() { return this.bindAddress; } @@ -240,55 +204,12 @@ public class Settings { return this.maxPeers; } + public String getBlockchainConfig() { + return this.blockchainConfig; + } + public boolean useBitcoinTestNet() { return this.useBitcoinTestNet; } - public boolean getWipeUnconfirmedOnStart() { - return this.wipeUnconfirmedOnStart; - } - - public int getMaxUnconfirmedPerAccount() { - return this.maxUnconfirmedPerAccount; - } - - public long getMaxTransactionTimestampFuture() { - return this.maxTransactionTimestampFuture; - } - - // Config parsing - - public static Object getTypedJson(JSONObject json, String key, Class clazz) { - if (!json.containsKey(key)) { - LOGGER.error("Missing \"" + key + "\" in blockchain config file"); - throw new RuntimeException("Missing \"" + key + "\" in blockchain config file"); - } - - Object value = json.get(key); - if (!clazz.isInstance(value)) { - LOGGER.error("\"" + key + "\" not " + clazz.getSimpleName() + " in blockchain config file"); - throw new RuntimeException("\"" + key + "\" not " + clazz.getSimpleName() + " in blockchain config file"); - } - - return value; - } - - public static BigDecimal getJsonBigDecimal(JSONObject json, String key) { - try { - return new BigDecimal((String) getTypedJson(json, key, String.class)); - } catch (NumberFormatException e) { - LOGGER.error("Unable to parse \"" + key + "\" in blockchain config file"); - throw new RuntimeException("Unable to parse \"" + key + "\" in blockchain config file"); - } - } - - public static Long getJsonQuotedLong(JSONObject json, String key) { - try { - return Long.parseLong((String) getTypedJson(json, key, String.class)); - } catch (NumberFormatException e) { - LOGGER.error("Unable to parse \"" + key + "\" in blockchain config file"); - throw new RuntimeException("Unable to parse \"" + key + "\" in blockchain config file"); - } - } - } diff --git a/src/main/java/org/qora/transaction/ArbitraryTransaction.java b/src/main/java/org/qora/transaction/ArbitraryTransaction.java index e69ce8d1..2391e741 100644 --- a/src/main/java/org/qora/transaction/ArbitraryTransaction.java +++ b/src/main/java/org/qora/transaction/ArbitraryTransaction.java @@ -149,7 +149,7 @@ public class ArbitraryTransaction extends Transaction { Account sender = this.getSender(); int blockHeight = this.repository.getBlockRepository().getBlockchainHeight(); - String senderPathname = Settings.getInstance().getUserpath() + "arbitrary" + File.separator + sender.getAddress(); + String senderPathname = Settings.getInstance().getUserPath() + "arbitrary" + File.separator + sender.getAddress(); String blockPathname = senderPathname + File.separator + blockHeight; String dataPathname = blockPathname + File.separator + Base58.encode(arbitraryTransactionData.getSignature()) + "-" + arbitraryTransactionData.getService() + ".raw"; @@ -187,7 +187,7 @@ public class ArbitraryTransaction extends Transaction { Account sender = this.getSender(); int blockHeight = this.repository.getBlockRepository().getBlockchainHeight(); - String senderPathname = Settings.getInstance().getUserpath() + "arbitrary" + File.separator + sender.getAddress(); + String senderPathname = Settings.getInstance().getUserPath() + "arbitrary" + File.separator + sender.getAddress(); String blockPathname = senderPathname + File.separator + blockHeight; String dataPathname = blockPathname + File.separator + Base58.encode(arbitraryTransactionData.getSignature()) + "-" + arbitraryTransactionData.getService() + ".raw"; diff --git a/src/main/java/org/qora/transaction/Transaction.java b/src/main/java/org/qora/transaction/Transaction.java index 667236bb..d73b0b33 100644 --- a/src/main/java/org/qora/transaction/Transaction.java +++ b/src/main/java/org/qora/transaction/Transaction.java @@ -184,6 +184,7 @@ public abstract class Transaction { INVALID_GROUP_ID(64), TRANSACTION_UNKNOWN(65), TRANSACTION_ALREADY_CONFIRMED(66), + INVALID_TX_GROUP_ID(67), NOT_YET_RELEASED(1000); public final int value; @@ -480,7 +481,7 @@ public abstract class Transaction { // Check transaction's txGroupId if (!this.isValidTxGroupId()) - return ValidationResult.INVALID_GROUP_ID; + return ValidationResult.INVALID_TX_GROUP_ID; creator.setLastReference(creator.getUnconfirmedLastReference()); ValidationResult result = this.isValid(); @@ -498,19 +499,19 @@ public abstract class Transaction { private boolean isValidTxGroupId() throws DataException { int txGroupId = this.transactionData.getTxGroupId(); + // If transaction type doesn't need approval then we insist on NO_GROUP + if (!this.transactionData.getType().needsApproval && txGroupId != Group.NO_GROUP) + return false; + // Handling NO_GROUP if (txGroupId == Group.NO_GROUP) - // true if NO_GROUP allowed, false otherwise - return BlockChain.getInstance().getGrouplessAllowed(); + // true if NO_GROUP txGroupId is allowed for approval-needing tx types + return !BlockChain.getInstance().getRequireGroupForApproval(); // Group even exist? if (!this.repository.getGroupRepository().groupExists(txGroupId)) return false; - // Does this transaction type bypass approval? - if (!this.transactionData.getType().needsApproval) - return true; - GroupRepository groupRepository = this.repository.getGroupRepository(); // Is transaction's creator is group member? @@ -642,6 +643,9 @@ public abstract class Transaction { /** * Returns whether transaction needs to go through group-admin approval. + *

+ * This test is more than simply "does this transaction type need approval?" + * because group admins bypass approval for transactions attached to their group. * * @throws DataException */ @@ -659,7 +663,7 @@ public abstract class Transaction { if (!groupRepository.groupExists(txGroupId)) // Group no longer exists? Possibly due to blockchain orphaning undoing group creation? - return true; + return true; // stops tx being included in block but it will eventually expire // If transaction's creator is group admin then auto-approve PublicKeyAccount creator = this.getCreator(); @@ -678,7 +682,8 @@ public abstract class Transaction { // Is transaction is outside of min/max approval period? int creationBlockHeight = this.repository.getBlockRepository().getHeightFromTimestamp(this.transactionData.getTimestamp()); int currentBlockHeight = this.repository.getBlockRepository().getBlockchainHeight(); - if (currentBlockHeight < creationBlockHeight + groupData.getMinimumBlockDelay() || currentBlockHeight > creationBlockHeight + groupData.getMaximumBlockDelay()) + if (currentBlockHeight < creationBlockHeight + groupData.getMinimumBlockDelay() + || currentBlockHeight > creationBlockHeight + groupData.getMaximumBlockDelay()) return false; return group.getGroupData().getApprovalThreshold().meetsApprovalThreshold(repository, txGroupId, this.transactionData.getSignature()); diff --git a/src/main/java/org/qora/utils/StringLongMapXmlAdapter.java b/src/main/java/org/qora/utils/StringLongMapXmlAdapter.java new file mode 100644 index 00000000..e5757f2d --- /dev/null +++ b/src/main/java/org/qora/utils/StringLongMapXmlAdapter.java @@ -0,0 +1,53 @@ +package org.qora.utils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import javax.xml.bind.annotation.XmlTransient; +import javax.xml.bind.annotation.XmlValue; +import javax.xml.bind.annotation.adapters.XmlAdapter; + +import org.eclipse.persistence.oxm.annotations.XmlVariableNode; + +public class StringLongMapXmlAdapter extends XmlAdapter> { + + public static class StringLongMap { + @XmlVariableNode("key") + List entries = new ArrayList(); + } + + public static class MapEntry { + @XmlTransient + public String key; + + @XmlValue + public Long value; + } + + @Override + public Map unmarshal(StringLongMap stringLongMap) throws Exception { + Map map = new HashMap<>(stringLongMap.entries.size()); + + for (MapEntry entry : stringLongMap.entries) + map.put(entry.key, entry.value); + + return map; + } + + @Override + public StringLongMap marshal(Map map) throws Exception { + StringLongMap output = new StringLongMap(); + + for (Entry entry : map.entrySet()) { + MapEntry mapEntry = new MapEntry(); + mapEntry.key = entry.getKey(); + mapEntry.value = entry.getValue(); + output.entries.add(mapEntry); + } + + return output; + } +} diff --git a/src/test/java/org/qora/test/ATTests.java b/src/test/java/org/qora/test/ATTests.java index fda75011..e759a928 100644 --- a/src/test/java/org/qora/test/ATTests.java +++ b/src/test/java/org/qora/test/ATTests.java @@ -1,6 +1,6 @@ package org.qora.test; -import org.junit.jupiter.api.Test; +import org.junit.Test; import org.qora.asset.Asset; import org.qora.data.at.ATStateData; import org.qora.data.block.BlockData; @@ -14,7 +14,7 @@ import org.qora.transaction.DeployAtTransaction; import org.qora.transform.TransformationException; import org.qora.utils.Base58; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.Assert.*; import java.math.BigDecimal; import java.util.Arrays; diff --git a/src/test/java/org/qora/test/BlockTests.java b/src/test/java/org/qora/test/BlockTests.java index ede8d4be..6487aaa5 100644 --- a/src/test/java/org/qora/test/BlockTests.java +++ b/src/test/java/org/qora/test/BlockTests.java @@ -3,7 +3,7 @@ package org.qora.test; import java.math.BigDecimal; import java.util.List; -import org.junit.jupiter.api.Test; +import org.junit.Test; import org.qora.block.Block; import org.qora.block.GenesisBlock; import org.qora.data.block.BlockData; @@ -15,7 +15,7 @@ import org.qora.transaction.Transaction; import org.qora.transform.TransformationException; import org.qora.transform.block.BlockTransformer; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.Assert.*; public class BlockTests extends Common { @@ -66,7 +66,7 @@ public class BlockTests extends Common { // Block 949 has lots of varied transactions // Blocks 390 & 754 have only payment transactions BlockData blockData = repository.getBlockRepository().fromHeight(754); - assertNotNull(blockData, "Block 754 is required for this test"); + assertNotNull("Block 754 is required for this test", blockData); Block block = new Block(repository, blockData); assertTrue(block.isSignatureValid()); @@ -107,7 +107,7 @@ public class BlockTests extends Common { // Block 949 has lots of varied transactions // Blocks 390 & 754 have only payment transactions BlockData blockData = repository.getBlockRepository().fromHeight(754); - assertNotNull(blockData, "Block 754 is required for this test"); + assertNotNull("Block 754 is required for this test", blockData); Block block = new Block(repository, blockData); assertTrue(block.isSignatureValid()); diff --git a/src/test/java/org/qora/test/BlockchainTests.java b/src/test/java/org/qora/test/BlockchainTests.java index 6df80569..f80965bb 100644 --- a/src/test/java/org/qora/test/BlockchainTests.java +++ b/src/test/java/org/qora/test/BlockchainTests.java @@ -1,6 +1,6 @@ package org.qora.test; -import org.junit.jupiter.api.Test; +import org.junit.Test; import org.qora.block.BlockChain; import org.qora.repository.DataException; diff --git a/src/test/java/org/qora/test/Common.java b/src/test/java/org/qora/test/Common.java index 62d39d18..93a0ee3c 100644 --- a/src/test/java/org/qora/test/Common.java +++ b/src/test/java/org/qora/test/Common.java @@ -1,24 +1,54 @@ package org.qora.test; -import org.junit.jupiter.api.BeforeAll; -import org.qora.controller.Controller; +import static org.junit.Assert.assertEquals; + +import java.security.Security; + +import org.bitcoinj.core.Base58; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; +import org.junit.AfterClass; +import org.junit.BeforeClass; import org.qora.repository.DataException; +import org.qora.repository.Repository; import org.qora.repository.RepositoryFactory; import org.qora.repository.RepositoryManager; import org.qora.repository.hsqldb.HSQLDBRepositoryFactory; -import org.junit.jupiter.api.AfterAll; +import org.qora.settings.Settings; public class Common { - @BeforeAll + public static final String testConnectionUrl = "jdbc:hsqldb:mem:testdb"; + public static final String testSettingsFilename = "test-settings.json"; + + public static final byte[] v2testPrivateKey = Base58.decode("A9MNsATgQgruBUjxy2rjWY36Yf19uRioKZbiLFT2P7c6"); + public static final byte[] v2testPublicKey = Base58.decode("2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP"); + public static final String v2testAddress = "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v"; + + static { + // This must go before any calls to LogManager/Logger + System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager"); + + Security.insertProviderAt(new BouncyCastleProvider(), 0); + Security.insertProviderAt(new BouncyCastleJsseProvider(), 1); + + // Load/check settings, which potentially sets up blockchain config, etc. + Settings.fileInstance(testSettingsFilename); + } + + @BeforeClass public static void setRepository() throws DataException { - RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.connectionUrl); + RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(testConnectionUrl); RepositoryManager.setRepositoryFactory(repositoryFactory); } - @AfterAll + @AfterClass public static void closeRepository() throws DataException { RepositoryManager.closeRepositoryFactory(); } + public static void assetEmptyBlockchain(Repository repository) throws DataException { + assertEquals("Blockchain should be empty for this test", 0, repository.getBlockRepository().getBlockchainHeight()); + } + } diff --git a/src/test/java/org/qora/test/CompatibilityTests.java b/src/test/java/org/qora/test/CompatibilityTests.java index 49737dbf..6fbafcb2 100644 --- a/src/test/java/org/qora/test/CompatibilityTests.java +++ b/src/test/java/org/qora/test/CompatibilityTests.java @@ -1,6 +1,6 @@ package org.qora.test; -import org.junit.jupiter.api.Test; +import org.junit.Test; import org.qora.data.transaction.TransactionData; import org.qora.transaction.CreateAssetOrderTransaction; import org.qora.transaction.CreatePollTransaction; @@ -8,7 +8,7 @@ import org.qora.transaction.IssueAssetTransaction; import org.qora.transform.TransformationException; import org.qora.transform.transaction.TransactionTransformer; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.Assert.*; import com.google.common.hash.HashCode; diff --git a/src/test/java/org/qora/test/CryptoTests.java b/src/test/java/org/qora/test/CryptoTests.java index be7543af..bbb25ef2 100644 --- a/src/test/java/org/qora/test/CryptoTests.java +++ b/src/test/java/org/qora/test/CryptoTests.java @@ -1,16 +1,17 @@ package org.qora.test; -import org.junit.jupiter.api.Test; +import org.junit.Test; +import org.qora.block.BlockChain; import org.qora.crypto.Crypto; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.Assert.*; import com.google.common.hash.HashCode; -public class CryptoTests { +public class CryptoTests extends Common { @Test - public void testCryptoDigest() { + public void testDigest() { byte[] input = HashCode.fromString("00").asBytes(); byte[] digest = Crypto.digest(input); byte[] expected = HashCode.fromString("6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d").asBytes(); @@ -19,7 +20,7 @@ public class CryptoTests { } @Test - public void testCryptoDoubleDigest() { + public void testDoubleDigest() { byte[] input = HashCode.fromString("00").asBytes(); byte[] digest = Crypto.doubleDigest(input); byte[] expected = HashCode.fromString("1406e05881e299367766d313e26c05564ec91bf721d31726bd6e46e60689539a").asBytes(); @@ -28,9 +29,9 @@ public class CryptoTests { } @Test - public void testCryptoQoraAddress() { + public void testPublicKeyToAddress() { byte[] publicKey = HashCode.fromString("775ada64a48a30b3bfc4f1db16bca512d4088704975a62bde78781ce0cba90d6").asBytes(); - String expected = "QUD9y7NZqTtNwvSAUfewd7zKUGoVivVnTW"; + String expected = BlockChain.getUseBrokenMD160ForAddresses() ? "QUD9y7NZqTtNwvSAUfewd7zKUGoVivVnTW" : "QPc6TvGJ5RjW6LpwUtafx7XRCdRvyN6rsA"; assertEquals(expected, Crypto.toAddress(publicKey)); } diff --git a/src/test/java/org/qora/test/ExceptionTests.java b/src/test/java/org/qora/test/ExceptionTests.java deleted file mode 100644 index eb6e13bb..00000000 --- a/src/test/java/org/qora/test/ExceptionTests.java +++ /dev/null @@ -1,90 +0,0 @@ -package org.qora.test; - -import org.junit.jupiter.api.Test; -import org.qora.block.Block; - -import static org.junit.jupiter.api.Assertions.*; - -public class ExceptionTests { - - /** - * Proof of concept for block processing throwing transaction-related SQLException rather than savepoint-rollback-related SQLException. - *

- * See {@link Block#isValid(Connection)}. - */ - @Test - public void testBlockProcessingExceptions() { - try { - simulateThrow(); - fail("Should not return result"); - } catch (Exception e) { - assertEquals("Transaction issue", e.getMessage()); - } - - try { - boolean result = simulateFalse(); - assertFalse(result); - } catch (Exception e) { - fail("Unexpected exception: " + e.getMessage()); - } - - try { - boolean result = simulateTrue(); - assertTrue(result); - } catch (Exception e) { - fail("Unexpected exception: " + e.getMessage()); - } - - } - - public boolean simulateThrow() throws Exception { - // simulate create savepoint (no-op) - - try { - // simulate processing transactions but an exception is thrown - throw new Exception("Transaction issue"); - } finally { - // attempt to rollback - try { - // simulate failing to rollback due to prior exception - throw new Exception("Rollback issue"); - } catch (Exception e) { - // test discard of rollback exception, leaving prior exception - } - } - } - - public boolean simulateFalse() throws Exception { - // simulate create savepoint (no-op) - - try { - // simulate processing transactions but false returned - return false; - } finally { - // attempt to rollback - try { - // simulate successful rollback (no-op) - } catch (Exception e) { - // test discard of rollback exception, leaving prior exception - } - } - } - - public boolean simulateTrue() throws Exception { - // simulate create savepoint (no-op) - - try { - // simulate processing transactions successfully - } finally { - // attempt to rollback - try { - // simulate successful rollback (no-op) - } catch (Exception e) { - // test discard of rollback exception, leaving prior exception - } - } - - return true; - } - -} diff --git a/src/test/java/org/qora/test/GenesisTests.java b/src/test/java/org/qora/test/GenesisTests.java index f7dcf455..59df1090 100644 --- a/src/test/java/org/qora/test/GenesisTests.java +++ b/src/test/java/org/qora/test/GenesisTests.java @@ -3,53 +3,33 @@ package org.qora.test; import java.math.BigDecimal; import java.util.List; -import org.junit.jupiter.api.Test; -import org.qora.account.Account; -import org.qora.asset.Asset; +import org.junit.Test; import org.qora.block.Block; import org.qora.block.GenesisBlock; import org.qora.data.transaction.TransactionData; import org.qora.repository.DataException; import org.qora.repository.Repository; -import org.qora.repository.RepositoryFactory; import org.qora.repository.RepositoryManager; -import org.qora.repository.hsqldb.HSQLDBRepositoryFactory; import org.qora.transaction.Transaction; -import static org.junit.jupiter.api.Assertions.*; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.AfterAll; +import static org.junit.Assert.*; -// Don't extend Common as we want an in-memory database -public class GenesisTests { - - public static final String connectionUrl = "jdbc:hsqldb:mem:db/blockchain;create=true"; - - @BeforeAll - public static void setRepository() throws DataException { - RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(connectionUrl); - RepositoryManager.setRepositoryFactory(repositoryFactory); - } - - @AfterAll - public static void closeRepository() throws DataException { - RepositoryManager.closeRepositoryFactory(); - } +public class GenesisTests extends Common { @Test public void testGenesisBlockTransactions() throws DataException { try (final Repository repository = RepositoryManager.getRepository()) { - assertEquals(0, repository.getBlockRepository().getBlockchainHeight(), "Blockchain should be empty for this test"); + assertEquals("Blockchain should be empty for this test", 0, repository.getBlockRepository().getBlockchainHeight()); GenesisBlock block = GenesisBlock.getInstance(repository); - assertNotNull(block); + assertNotNull("No genesis block?", block); assertTrue(block.isSignatureValid()); // Note: only true if blockchain is empty - assertEquals(Block.ValidationResult.OK, block.isValid()); + assertEquals("Block invalid", Block.ValidationResult.OK, block.isValid()); List transactions = block.getTransactions(); - assertNotNull(transactions); + assertNotNull("No transactions?", transactions); for (Transaction transaction : transactions) { assertNotNull(transaction); @@ -67,26 +47,6 @@ public class GenesisTests { // Actually try to process genesis block onto empty blockchain block.process(); repository.saveChanges(); - - // Attempt to load first transaction directly from database - TransactionData transactionData = repository.getTransactionRepository().fromSignature(transactions.get(0).getTransactionData().getSignature()); - assertNotNull(transactionData); - - assertEquals(Transaction.TransactionType.GENESIS, transactionData.getType()); - assertTrue(transactionData.getFee().compareTo(BigDecimal.ZERO) == 0); - assertNull(transactionData.getReference()); - - Transaction transaction = Transaction.fromData(repository, transactionData); - assertNotNull(transaction); - - assertTrue(transaction.isSignatureValid()); - assertEquals(Transaction.ValidationResult.OK, transaction.isValid()); - - // Check known balance - Account testAccount = new Account(repository, "QegT2Ws5YjLQzEZ9YMzWsAZMBE8cAygHZN"); - BigDecimal testBalance = testAccount.getConfirmedBalance(Asset.QORA); - BigDecimal expectedBalance = new BigDecimal("12606834").setScale(8); - assertTrue(testBalance.compareTo(expectedBalance) == 0); } } diff --git a/src/test/java/org/qora/test/GroupApprovalTests.java b/src/test/java/org/qora/test/GroupApprovalTests.java new file mode 100644 index 00000000..695a2163 --- /dev/null +++ b/src/test/java/org/qora/test/GroupApprovalTests.java @@ -0,0 +1,99 @@ +package org.qora.test; + +import org.junit.Test; +import org.qora.account.PrivateKeyAccount; +import org.qora.block.BlockChain; +import org.qora.block.BlockGenerator; +import org.qora.data.transaction.CreateGroupTransactionData; +import org.qora.data.transaction.PaymentTransactionData; +import org.qora.data.transaction.TransactionData; +import org.qora.group.Group; +import org.qora.group.Group.ApprovalThreshold; +import org.qora.repository.DataException; +import org.qora.repository.Repository; +import org.qora.repository.RepositoryManager; +import org.qora.transaction.CreateGroupTransaction; +import org.qora.transaction.PaymentTransaction; +import org.qora.transaction.Transaction; +import org.qora.transaction.Transaction.ValidationResult; + +import static org.junit.Assert.*; + +import java.math.BigDecimal; + +public class GroupApprovalTests extends Common { + + /** Check that a tx type that doesn't need approval doesn't accept txGroupId apart from NO_GROUP */ + @Test + public void testNonApprovalTxGroupId() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + BlockChain.validate(); + + TransactionData transactionData = buildPayment(repository, Group.NO_GROUP); + Transaction transaction = new PaymentTransaction(repository, transactionData); + assertEquals(ValidationResult.OK, transaction.isValidUnconfirmed()); + + int groupId = createGroup(repository); + + transactionData = buildPayment(repository, groupId); + transaction = new PaymentTransaction(repository, transactionData); + assertEquals(ValidationResult.INVALID_TX_GROUP_ID, transaction.isValidUnconfirmed()); + } + } + + private PaymentTransactionData buildPayment(Repository repository, int txGroupId) throws DataException { + long timestamp = System.currentTimeMillis() - 1000L; + byte[] reference = repository.getAccountRepository().getLastReference(v2testAddress); + byte[] senderPublicKey = v2testPublicKey; + String recipient = v2testAddress; + BigDecimal amount = BigDecimal.ONE.setScale(8); + BigDecimal fee = BigDecimal.ONE.setScale(8); + + return new PaymentTransactionData(timestamp, txGroupId, reference, senderPublicKey, recipient, amount, fee); + } + + private int createGroup(Repository repository) throws DataException { + long timestamp = System.currentTimeMillis() - 1000L; + int txGroupId = Group.NO_GROUP; + byte[] reference = repository.getAccountRepository().getLastReference(v2testAddress); + byte[] creatorPublicKey = v2testPublicKey; + String owner = v2testAddress; + String groupName = "test-group"; + String description = "test group description"; + boolean isOpen = false; + ApprovalThreshold approvalThreshold = ApprovalThreshold.ONE; + int minimumBlockDelay = 0; + int maximumBlockDelay = 1440; + Integer groupId = null; + BigDecimal fee = BigDecimal.ONE.setScale(8); + byte[] signature = null; + + TransactionData transactionData = new CreateGroupTransactionData(timestamp, txGroupId, reference, creatorPublicKey, owner, groupName, description, + isOpen, approvalThreshold, minimumBlockDelay, maximumBlockDelay, groupId, fee, signature); + Transaction transaction = new CreateGroupTransaction(repository, transactionData); + + // Sign transaction + PrivateKeyAccount signer = new PrivateKeyAccount(repository, v2testPrivateKey); + transaction.sign(signer); + + // Add to unconfirmed + if (!transaction.isSignatureValid()) + throw new RuntimeException("CREATE_GROUP transaction's signature invalid"); + + ValidationResult result = transaction.isValidUnconfirmed(); + if (result != ValidationResult.OK) + throw new RuntimeException(String.format("CREATE_GROUP transaction invalid: %s", result.name())); + + repository.getTransactionRepository().save(transactionData); + repository.getTransactionRepository().unconfirmTransaction(transactionData); + repository.saveChanges(); + + // Generate block + BlockGenerator.generateTestingBlock(repository, signer); + + // Return assigned groupId + transactionData = repository.getTransactionRepository().fromSignature(transactionData.getSignature()); + return ((CreateGroupTransactionData) transactionData).getGroupId(); + } + +} diff --git a/src/test/java/org/qora/test/LoadTests.java b/src/test/java/org/qora/test/LoadTests.java index ce55b1e9..63b764a5 100644 --- a/src/test/java/org/qora/test/LoadTests.java +++ b/src/test/java/org/qora/test/LoadTests.java @@ -1,6 +1,6 @@ package org.qora.test; -import org.junit.jupiter.api.Test; +import org.junit.Test; import org.qora.account.PublicKeyAccount; import org.qora.data.transaction.PaymentTransactionData; import org.qora.data.transaction.TransactionData; @@ -11,7 +11,7 @@ import org.qora.repository.TransactionRepository; import org.qora.transaction.Transaction.TransactionType; import org.qora.utils.Base58; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.Assert.*; public class LoadTests extends Common { @@ -20,15 +20,14 @@ public class LoadTests extends Common { try (final Repository repository = RepositoryManager.getRepository()) { TransactionRepository transactionRepository = repository.getTransactionRepository(); - assertTrue(repository.getBlockRepository().getBlockchainHeight() >= 49778, - "Migrate from old database to at least block 49778 before running this test"); + assertTrue("Migrate from old database to at least block 49778 before running this test", repository.getBlockRepository().getBlockchainHeight() >= 49778); String signature58 = "1211ZPwG3hk5evWzXCZi9hMDRpwumWmkENjwWkeTCik9xA5uoYnxzF7rwR5hmHH3kG2RXo7ToCAaRc7dvnynByJt"; byte[] signature = Base58.decode(signature58); TransactionData transactionData = transactionRepository.fromSignature(signature); - assertNotNull(transactionData, "Transaction data not loaded from repository"); - assertEquals(TransactionType.PAYMENT, transactionData.getType(), "Transaction data not PAYMENT type"); + assertNotNull("Transaction data not loaded from repository", transactionData); + assertEquals("Transaction data not PAYMENT type", TransactionType.PAYMENT, transactionData.getType()); assertEquals("QXwu8924WdgPoRmtiWQBUMF6eedmp1Hu2E", PublicKeyAccount.getAddress(transactionData.getCreatorPublicKey())); PaymentTransactionData paymentTransactionData = (PaymentTransactionData) transactionData; @@ -47,8 +46,7 @@ public class LoadTests extends Common { try (final Repository repository = RepositoryManager.getRepository()) { TransactionRepository transactionRepository = repository.getTransactionRepository(); - assertTrue(repository.getBlockRepository().getBlockchainHeight() >= 49778, - "Migrate from old database to at least block 49778 before running this test"); + assertTrue("Migrate from old database to at least block 49778 before running this test", repository.getBlockRepository().getBlockchainHeight() >= 49778); String signature58 = "1211ZPwG3hk5evWzXCZi9hMDRpwumWmkENjwWkeTCik9xA5uoYnxzF7rwR5hmHH3kG2RXo7ToCAaRc7dvnynByJt"; byte[] signature = Base58.decode(signature58); diff --git a/src/test/java/org/qora/test/NavigationTests.java b/src/test/java/org/qora/test/NavigationTests.java index 932ae0b7..64db0a22 100644 --- a/src/test/java/org/qora/test/NavigationTests.java +++ b/src/test/java/org/qora/test/NavigationTests.java @@ -1,6 +1,6 @@ package org.qora.test; -import org.junit.jupiter.api.Test; +import org.junit.Test; import org.qora.data.block.BlockData; import org.qora.data.transaction.TransactionData; import org.qora.repository.DataException; @@ -10,7 +10,7 @@ import org.qora.repository.TransactionRepository; import org.qora.transaction.Transaction.TransactionType; import org.qora.utils.Base58; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.Assert.*; public class NavigationTests extends Common { @@ -19,8 +19,7 @@ public class NavigationTests extends Common { try (final Repository repository = RepositoryManager.getRepository()) { TransactionRepository transactionRepository = repository.getTransactionRepository(); - assertTrue(repository.getBlockRepository().getBlockchainHeight() >= 49778, - "Migrate from old database to at least block 49778 before running this test"); + assertTrue("Migrate from old database to at least block 49778 before running this test", repository.getBlockRepository().getBlockchainHeight() >= 49778); String signature58 = "1211ZPwG3hk5evWzXCZi9hMDRpwumWmkENjwWkeTCik9xA5uoYnxzF7rwR5hmHH3kG2RXo7ToCAaRc7dvnynByJt"; byte[] signature = Base58.decode(signature58); @@ -28,15 +27,15 @@ public class NavigationTests extends Common { System.out.println("Navigating to Block from transaction " + signature58); TransactionData transactionData = transactionRepository.fromSignature(signature); - assertNotNull(transactionData, "Transaction data not loaded from repository"); - assertEquals(TransactionType.PAYMENT, transactionData.getType(), "Transaction data not PAYMENT type"); + assertNotNull("Transaction data not loaded from repository", transactionData); + assertEquals("Transaction data not PAYMENT type", TransactionType.PAYMENT, transactionData.getType()); int transactionHeight = transactionRepository.getHeightFromSignature(signature); - assertNotEquals(0, transactionHeight, "Transaction not found or transaction's block not found"); - assertEquals(49778, transactionHeight, "Transaction's block height expected to be 49778"); + assertFalse("Transaction not found or transaction's block not found", transactionHeight == 0); + assertEquals("Transaction's block height expected to be 49778", 49778, transactionHeight); BlockData blockData = repository.getBlockRepository().fromHeight(transactionHeight); - assertNotNull(blockData, "Block 49778 not loaded from database"); + assertNotNull("Block 49778 not loaded from database", blockData); System.out.println("Block " + blockData.getHeight() + ", signature: " + Base58.encode(blockData.getSignature())); assertEquals((Integer) 49778, blockData.getHeight()); diff --git a/src/test/java/org/qora/test/RepositoryTests.java b/src/test/java/org/qora/test/RepositoryTests.java index b8f88335..5847086f 100644 --- a/src/test/java/org/qora/test/RepositoryTests.java +++ b/src/test/java/org/qora/test/RepositoryTests.java @@ -1,11 +1,11 @@ package org.qora.test; -import org.junit.jupiter.api.Test; +import org.junit.Test; import org.qora.repository.DataException; import org.qora.repository.Repository; import org.qora.repository.RepositoryManager; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.Assert.*; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; diff --git a/src/test/java/org/qora/test/SaveTests.java b/src/test/java/org/qora/test/SaveTests.java index d95b8074..ce6eb556 100644 --- a/src/test/java/org/qora/test/SaveTests.java +++ b/src/test/java/org/qora/test/SaveTests.java @@ -3,7 +3,7 @@ package org.qora.test; import java.math.BigDecimal; import java.time.Instant; -import org.junit.jupiter.api.Test; +import org.junit.Test; import org.qora.account.PublicKeyAccount; import org.qora.data.transaction.PaymentTransactionData; import org.qora.group.Group; diff --git a/src/test/java/org/qora/test/SerializationTests.java b/src/test/java/org/qora/test/SerializationTests.java index 7837af8e..8741e011 100644 --- a/src/test/java/org/qora/test/SerializationTests.java +++ b/src/test/java/org/qora/test/SerializationTests.java @@ -1,6 +1,6 @@ package org.qora.test; -import org.junit.jupiter.api.Test; +import org.junit.Test; import org.qora.block.Block; import org.qora.block.GenesisBlock; import org.qora.data.block.BlockData; @@ -15,7 +15,7 @@ import org.qora.transaction.Transaction.TransactionType; import org.qora.transform.TransformationException; import org.qora.transform.transaction.TransactionTransformer; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.Assert.*; import java.util.Arrays; import java.util.List; @@ -60,15 +60,15 @@ public class SerializationTests extends Common { TransactionData parsedTransactionData = TransactionTransformer.fromBytes(bytes); - assertTrue(Arrays.equals(transactionData.getSignature(), parsedTransactionData.getSignature()), "Transaction signature mismatch"); + assertTrue("Transaction signature mismatch", Arrays.equals(transactionData.getSignature(), parsedTransactionData.getSignature())); - assertEquals(bytes.length, TransactionTransformer.getDataLength(transactionData), "Data length mismatch"); + assertEquals("Data length mismatch", bytes.length, TransactionTransformer.getDataLength(transactionData)); } private void testSpecificBlockTransactions(int height, TransactionType type) throws DataException, TransformationException { try (final Repository repository = RepositoryManager.getRepository()) { BlockData blockData = repository.getBlockRepository().fromHeight(height); - assertNotNull(blockData, "Block " + height + " is required for this test"); + assertNotNull("Block " + height + " is required for this test", blockData); Block block = new Block(repository, blockData); diff --git a/src/test/java/org/qora/test/SettingsTests.java b/src/test/java/org/qora/test/SettingsTests.java new file mode 100644 index 00000000..8149a636 --- /dev/null +++ b/src/test/java/org/qora/test/SettingsTests.java @@ -0,0 +1,64 @@ +package org.qora.test; + +import java.io.FileReader; +import java.io.IOException; +import java.io.Reader; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Unmarshaller; +import javax.xml.transform.stream.StreamSource; + +import org.eclipse.persistence.jaxb.JAXBContextFactory; +import org.eclipse.persistence.jaxb.UnmarshallerProperties; +import org.qora.block.BlockChain; +import org.qora.data.transaction.TransactionData; +import org.qora.settings.Settings; + +public class SettingsTests { + + public static void main(String[] args) throws JAXBException, IOException { + // JAXBContext jc = JAXBContext.newInstance(SettingsData.class); + JAXBContext jc = JAXBContextFactory.createContext(new Class[] {Settings.class, BlockChain.class, TransactionData.class}, null); + + // Create the Unmarshaller Object using the JaxB Context + Unmarshaller unmarshaller = jc.createUnmarshaller(); + + // Set the Unmarshaller media type to JSON or XML + unmarshaller.setProperty(UnmarshallerProperties.MEDIA_TYPE, "application/json"); + + // Set it to true if you need to include the JSON root element in the JSON input + unmarshaller.setProperty(UnmarshallerProperties.JSON_INCLUDE_ROOT, false); + + Settings settings = null; + + // Create the StreamSource by creating Reader to the JSON input + try (Reader settingsReader = new FileReader("settings.json")) { + StreamSource json = new StreamSource(settingsReader); + + // Getting the SettingsData pojo from the json + settings = unmarshaller.unmarshal(json, Settings.class).getValue(); + + System.out.println("API settings:"); + System.out.println(String.format("Enabled: %s, port: %d, restricted: %s, whitelist: %s", yn(settings.isApiEnabled()), settings.getApiPort(), + yn(settings.isApiRestricted()), String.join(", ", settings.getApiWhitelist()))); + } + + String blockchainConfig = settings.getBlockchainConfig(); + if (blockchainConfig != null) + try (Reader settingsReader = new FileReader(blockchainConfig)) { + StreamSource json = new StreamSource(settingsReader); + + // Getting the BlockChainData pojo from the JSON + BlockChain blockchain = unmarshaller.unmarshal(json, BlockChain.class).getValue(); + + System.out.println("BlockChain settings:"); + System.out.println(String.format("TestNet: %s", yn(blockchain.isTestNet()))); + } + } + + private static String yn(boolean flag) { + return flag ? "yes" : "no"; + } + +} diff --git a/src/test/java/org/qora/test/SignatureTests.java b/src/test/java/org/qora/test/SignatureTests.java index 196a02ce..033b87bc 100644 --- a/src/test/java/org/qora/test/SignatureTests.java +++ b/src/test/java/org/qora/test/SignatureTests.java @@ -1,6 +1,6 @@ package org.qora.test; -import org.junit.jupiter.api.Test; +import org.junit.Test; import org.qora.account.PrivateKeyAccount; import org.qora.block.Block; import org.qora.block.GenesisBlock; @@ -11,7 +11,7 @@ import org.qora.repository.RepositoryManager; import org.qora.utils.Base58; import org.qora.utils.NTP; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.Assert.*; import java.math.BigDecimal; diff --git a/src/test/java/org/qora/test/TransactionTests.java b/src/test/java/org/qora/test/TransactionTests.java index c5b32d41..54a1c7dd 100644 --- a/src/test/java/org/qora/test/TransactionTests.java +++ b/src/test/java/org/qora/test/TransactionTests.java @@ -1,6 +1,7 @@ package org.qora.test; -import org.junit.jupiter.api.Test; +import org.junit.After; +import org.junit.Test; import org.qora.account.Account; import org.qora.account.PrivateKeyAccount; import org.qora.account.PublicKeyAccount; @@ -37,10 +38,7 @@ import org.qora.repository.AccountRepository; import org.qora.repository.AssetRepository; import org.qora.repository.DataException; import org.qora.repository.Repository; -import org.qora.repository.RepositoryFactory; import org.qora.repository.RepositoryManager; -import org.qora.repository.hsqldb.HSQLDBRepositoryFactory; -import org.qora.settings.Settings; import org.qora.transaction.BuyNameTransaction; import org.qora.transaction.CancelAssetOrderTransaction; import org.qora.transaction.CancelSellNameTransaction; @@ -58,8 +56,7 @@ import org.qora.transaction.UpdateNameTransaction; import org.qora.transaction.VoteOnPollTransaction; import org.qora.transaction.Transaction.ValidationResult; -import static org.junit.jupiter.api.Assertions.*; -import org.junit.jupiter.api.AfterEach; +import static org.junit.Assert.*; import java.io.UnsupportedEncodingException; import java.math.BigDecimal; @@ -68,14 +65,9 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import org.json.simple.JSONObject; - import com.google.common.hash.HashCode; -// Don't extend Common as we want to use an in-memory database -public class TransactionTests { - - private static final String connectionUrl = "jdbc:hsqldb:mem:db/blockchain;create=true"; +public class TransactionTests extends Common { private static final byte[] generatorSeed = HashCode.fromString("0123456789abcdeffedcba98765432100123456789abcdeffedcba9876543210").asBytes(); private static final byte[] senderSeed = HashCode.fromString("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef").asBytes(); @@ -92,22 +84,11 @@ public class TransactionTests { private PrivateKeyAccount generator; private byte[] reference; - @SuppressWarnings("unchecked") public void createTestAccounts(Long genesisTimestamp) throws DataException { - RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(connectionUrl); - RepositoryManager.setRepositoryFactory(repositoryFactory); - try (final Repository repository = RepositoryManager.getRepository()) { - assertEquals(0, repository.getBlockRepository().getBlockchainHeight(), "Blockchain should be empty for this test"); + assertEquals("Blockchain should be empty for this test", 0, repository.getBlockRepository().getBlockchainHeight()); } - // [Un]set genesis timestamp as required by test - JSONObject settingsJSON = new JSONObject(); - if (genesisTimestamp != null) - settingsJSON.put("testnetstamp", genesisTimestamp); - - Settings.test(settingsJSON); - // This needs to be called outside of acquiring our own repository or it will deadlock BlockChain.validate(); @@ -137,9 +118,9 @@ public class TransactionTests { repository.saveChanges(); } - @AfterEach - public void closeRepository() throws DataException { - RepositoryManager.closeRepositoryFactory(); + @After + public void afterTest() throws DataException { + repository.close(); } private Transaction createPayment(PrivateKeyAccount sender, String recipient) throws DataException { @@ -147,7 +128,8 @@ public class TransactionTests { BigDecimal amount = genericPaymentAmount; BigDecimal fee = BigDecimal.ONE; long timestamp = parentBlockData.getTimestamp() + 1_000; - PaymentTransactionData paymentTransactionData = new PaymentTransactionData(timestamp, Group.NO_GROUP, reference, sender.getPublicKey(), recipient, amount, fee); + PaymentTransactionData paymentTransactionData = new PaymentTransactionData(timestamp, Group.NO_GROUP, reference, sender.getPublicKey(), recipient, + amount, fee); Transaction paymentTransaction = new PaymentTransaction(repository, paymentTransactionData); paymentTransaction.sign(sender); @@ -164,8 +146,8 @@ public class TransactionTests { BigDecimal amount = BigDecimal.valueOf(1_000L); BigDecimal fee = BigDecimal.ONE; long timestamp = parentBlockData.getTimestamp() + 1_000; - PaymentTransactionData paymentTransactionData = new PaymentTransactionData(timestamp, Group.NO_GROUP, reference, sender.getPublicKey(), recipient.getAddress(), - amount, fee); + PaymentTransactionData paymentTransactionData = new PaymentTransactionData(timestamp, Group.NO_GROUP, reference, sender.getPublicKey(), + recipient.getAddress(), amount, fee); Transaction paymentTransaction = new PaymentTransaction(repository, paymentTransactionData); paymentTransaction.sign(sender); @@ -177,8 +159,8 @@ public class TransactionTests { block.addTransaction(paymentTransactionData); block.sign(); - assertTrue(block.isSignatureValid(), "Block signatures invalid"); - assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid"); + assertTrue("Block signatures invalid", block.isSignatureValid()); + assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid()); block.process(); repository.saveChanges(); @@ -186,21 +168,21 @@ public class TransactionTests { // Check sender's balance BigDecimal expectedBalance = initialSenderBalance.subtract(amount).subtract(fee); BigDecimal actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance(); - assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Sender's new balance incorrect"); + assertTrue("Sender's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); // Fee should be in generator's balance expectedBalance = initialGeneratorBalance.add(fee); actualBalance = accountRepository.getBalance(generator.getAddress(), Asset.QORA).getBalance(); - assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Generator's new balance incorrect"); + assertTrue("Generator's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); // Amount should be in recipient's balance expectedBalance = amount; actualBalance = accountRepository.getBalance(recipient.getAddress(), Asset.QORA).getBalance(); - assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Recipient's new balance incorrect"); + assertTrue("Recipient's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); // Check recipient's reference byte[] recipientsReference = recipient.getLastReference(); - assertTrue(Arrays.equals(paymentTransaction.getTransactionData().getSignature(), recipientsReference), "Recipient's new reference incorrect"); + assertTrue("Recipient's new reference incorrect", Arrays.equals(paymentTransaction.getTransactionData().getSignature(), recipientsReference)); // Orphan block block.orphan(); @@ -208,11 +190,11 @@ public class TransactionTests { // Check sender's balance actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance(); - assertTrue(initialSenderBalance.compareTo(actualBalance) == 0, "Sender's reverted balance incorrect"); + assertTrue("Sender's reverted balance incorrect", initialSenderBalance.compareTo(actualBalance) == 0); // Check generator's balance actualBalance = accountRepository.getBalance(generator.getAddress(), Asset.QORA).getBalance(); - assertTrue(initialGeneratorBalance.compareTo(actualBalance) == 0, "Generator's new balance incorrect"); + assertTrue("Generator's new balance incorrect", initialGeneratorBalance.compareTo(actualBalance) == 0); } @Test @@ -225,8 +207,8 @@ public class TransactionTests { BigDecimal fee = BigDecimal.ONE; long timestamp = parentBlockData.getTimestamp() + 1_000; - RegisterNameTransactionData registerNameTransactionData = new RegisterNameTransactionData(timestamp, Group.NO_GROUP, reference, sender.getPublicKey(), sender.getAddress(), - name, data, fee); + RegisterNameTransactionData registerNameTransactionData = new RegisterNameTransactionData(timestamp, Group.NO_GROUP, reference, sender.getPublicKey(), + sender.getAddress(), name, data, fee); Transaction registerNameTransaction = new RegisterNameTransaction(repository, registerNameTransactionData); registerNameTransaction.sign(sender); @@ -238,8 +220,8 @@ public class TransactionTests { block.addTransaction(registerNameTransactionData); block.sign(); - assertTrue(block.isSignatureValid(), "Block signatures invalid"); - assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid"); + assertTrue("Block signatures invalid", block.isSignatureValid()); + assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid()); block.process(); repository.saveChanges(); @@ -247,19 +229,19 @@ public class TransactionTests { // Check sender's balance BigDecimal expectedBalance = initialSenderBalance.subtract(fee); BigDecimal actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance(); - assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Sender's new balance incorrect"); + assertTrue("Sender's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); // Fee should be in generator's balance expectedBalance = initialGeneratorBalance.add(fee); actualBalance = accountRepository.getBalance(generator.getAddress(), Asset.QORA).getBalance(); - assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Generator's new balance incorrect"); + assertTrue("Generator's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); // Check name was registered NameData actualNameData = this.repository.getNameRepository().fromName(name); assertNotNull(actualNameData); // Check sender's reference - assertTrue(Arrays.equals(registerNameTransactionData.getSignature(), sender.getLastReference()), "Sender's new reference incorrect"); + assertTrue("Sender's new reference incorrect", Arrays.equals(registerNameTransactionData.getSignature(), sender.getLastReference())); // Update variables for use by other tests reference = sender.getLastReference(); @@ -294,8 +276,8 @@ public class TransactionTests { block.addTransaction(updateNameTransactionData); block.sign(); - assertTrue(block.isSignatureValid(), "Block signatures invalid"); - assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid"); + assertTrue("Block signatures invalid", block.isSignatureValid()); + assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid()); block.process(); repository.saveChanges(); @@ -327,7 +309,8 @@ public class TransactionTests { BigDecimal fee = BigDecimal.ONE; long timestamp = parentBlockData.getTimestamp() + 1_000; - SellNameTransactionData sellNameTransactionData = new SellNameTransactionData(timestamp, Group.NO_GROUP, reference, sender.getPublicKey(), name, amount, fee); + SellNameTransactionData sellNameTransactionData = new SellNameTransactionData(timestamp, Group.NO_GROUP, reference, sender.getPublicKey(), name, amount, + fee); Transaction sellNameTransaction = new SellNameTransaction(repository, sellNameTransactionData); sellNameTransaction.sign(sender); @@ -339,8 +322,8 @@ public class TransactionTests { block.addTransaction(sellNameTransactionData); block.sign(); - assertTrue(block.isSignatureValid(), "Block signatures invalid"); - assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid"); + assertTrue("Block signatures invalid", block.isSignatureValid()); + assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid()); block.process(); repository.saveChanges(); @@ -378,7 +361,8 @@ public class TransactionTests { BigDecimal fee = BigDecimal.ONE; long timestamp = parentBlockData.getTimestamp() + 1_000; - CancelSellNameTransactionData cancelSellNameTransactionData = new CancelSellNameTransactionData(timestamp, Group.NO_GROUP, reference, sender.getPublicKey(), name, fee); + CancelSellNameTransactionData cancelSellNameTransactionData = new CancelSellNameTransactionData(timestamp, Group.NO_GROUP, reference, + sender.getPublicKey(), name, fee); Transaction cancelSellNameTransaction = new CancelSellNameTransaction(repository, cancelSellNameTransactionData); cancelSellNameTransaction.sign(sender); @@ -390,8 +374,8 @@ public class TransactionTests { block.addTransaction(cancelSellNameTransactionData); block.sign(); - assertTrue(block.isSignatureValid(), "Block signatures invalid"); - assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid"); + assertTrue("Block signatures invalid", block.isSignatureValid()); + assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid()); block.process(); repository.saveChanges(); @@ -443,8 +427,8 @@ public class TransactionTests { BigDecimal fee = BigDecimal.ONE; long timestamp = parentBlockData.getTimestamp() + 1_000; - BuyNameTransactionData buyNameTransactionData = new BuyNameTransactionData(timestamp, Group.NO_GROUP, buyersReference, buyer.getPublicKey(), - name, originalNameData.getSalePrice(), seller, nameReference, fee); + BuyNameTransactionData buyNameTransactionData = new BuyNameTransactionData(timestamp, Group.NO_GROUP, buyersReference, buyer.getPublicKey(), name, + originalNameData.getSalePrice(), seller, nameReference, fee); Transaction buyNameTransaction = new BuyNameTransaction(repository, buyNameTransactionData); buyNameTransaction.sign(buyer); @@ -456,8 +440,8 @@ public class TransactionTests { block.addTransaction(buyNameTransactionData); block.sign(); - assertTrue(block.isSignatureValid(), "Block signatures invalid"); - assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid"); + assertTrue("Block signatures invalid", block.isSignatureValid()); + assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid()); block.process(); repository.saveChanges(); @@ -496,8 +480,8 @@ public class TransactionTests { Account recipient = new PublicKeyAccount(repository, recipientSeed); BigDecimal fee = BigDecimal.ONE; long timestamp = parentBlockData.getTimestamp() + 1_000; - CreatePollTransactionData createPollTransactionData = new CreatePollTransactionData(timestamp, Group.NO_GROUP, reference, - sender.getPublicKey(), recipient.getAddress(), pollName, description, pollOptions, fee); + CreatePollTransactionData createPollTransactionData = new CreatePollTransactionData(timestamp, Group.NO_GROUP, reference, sender.getPublicKey(), + recipient.getAddress(), pollName, description, pollOptions, fee); Transaction createPollTransaction = new CreatePollTransaction(repository, createPollTransactionData); createPollTransaction.sign(sender); @@ -509,8 +493,8 @@ public class TransactionTests { block.addTransaction(createPollTransactionData); block.sign(); - assertTrue(block.isSignatureValid(), "Block signatures invalid"); - assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid"); + assertTrue("Block signatures invalid", block.isSignatureValid()); + assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid()); block.process(); repository.saveChanges(); @@ -518,19 +502,19 @@ public class TransactionTests { // Check sender's balance BigDecimal expectedBalance = initialSenderBalance.subtract(fee); BigDecimal actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance(); - assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Sender's new balance incorrect"); + assertTrue("Sender's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); // Fee should be in generator's balance expectedBalance = initialGeneratorBalance.add(fee); actualBalance = accountRepository.getBalance(generator.getAddress(), Asset.QORA).getBalance(); - assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Generator's new balance incorrect"); + assertTrue("Generator's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); // Check poll was created PollData actualPollData = this.repository.getVotingRepository().fromPollName(pollName); assertNotNull(actualPollData); // Check sender's reference - assertTrue(Arrays.equals(createPollTransactionData.getSignature(), sender.getLastReference()), "Sender's new reference incorrect"); + assertTrue("Sender's new reference incorrect", Arrays.equals(createPollTransactionData.getSignature(), sender.getLastReference())); // Update variables for use by other tests reference = sender.getLastReference(); @@ -550,8 +534,8 @@ public class TransactionTests { for (int optionIndex = 0; optionIndex <= pollOptionsSize; ++optionIndex) { // Make a vote-on-poll transaction - VoteOnPollTransactionData voteOnPollTransactionData = new VoteOnPollTransactionData(timestamp, Group.NO_GROUP, reference, sender.getPublicKey(), pollName, - optionIndex, fee); + VoteOnPollTransactionData voteOnPollTransactionData = new VoteOnPollTransactionData(timestamp, Group.NO_GROUP, reference, sender.getPublicKey(), + pollName, optionIndex, fee); Transaction voteOnPollTransaction = new VoteOnPollTransaction(repository, voteOnPollTransactionData); voteOnPollTransaction.sign(sender); @@ -568,8 +552,8 @@ public class TransactionTests { block.addTransaction(voteOnPollTransactionData); block.sign(); - assertTrue(block.isSignatureValid(), "Block signatures invalid"); - assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid"); + assertTrue("Block signatures invalid", block.isSignatureValid()); + assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid()); block.process(); repository.saveChanges(); @@ -589,10 +573,10 @@ public class TransactionTests { List votes = repository.getVotingRepository().getVotes(pollName); assertNotNull(votes); - assertEquals(1, votes.size(), "Only one vote expected"); + assertEquals("Only one vote expected", 1, votes.size()); - assertEquals(pollOptionsSize - 1, votes.get(0).getOptionIndex(), "Wrong vote option index"); - assertTrue(Arrays.equals(sender.getPublicKey(), votes.get(0).getVoterPublicKey()), "Wrong voter public key"); + assertEquals("Wrong vote option index", pollOptionsSize - 1, votes.get(0).getOptionIndex()); + assertTrue("Wrong voter public key", Arrays.equals(sender.getPublicKey(), votes.get(0).getVoterPublicKey())); // Orphan last block BlockData lastBlockData = repository.getBlockRepository().getLastBlock(); @@ -604,10 +588,10 @@ public class TransactionTests { votes = repository.getVotingRepository().getVotes(pollName); assertNotNull(votes); - assertEquals(1, votes.size(), "Only one vote expected"); + assertEquals("Only one vote expected", 1, votes.size()); - assertEquals(pollOptionsSize - 1 - 1, votes.get(0).getOptionIndex(), "Wrong vote option index"); - assertTrue(Arrays.equals(sender.getPublicKey(), votes.get(0).getVoterPublicKey()), "Wrong voter public key"); + assertEquals("Wrong vote option index", pollOptionsSize - 1 - 1, votes.get(0).getOptionIndex()); + assertTrue("Wrong voter public key", Arrays.equals(sender.getPublicKey(), votes.get(0).getVoterPublicKey())); } @Test @@ -635,8 +619,8 @@ public class TransactionTests { block.addTransaction(issueAssetTransactionData); block.sign(); - assertTrue(block.isSignatureValid(), "Block signatures invalid"); - assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid"); + assertTrue("Block signatures invalid", block.isSignatureValid()); + assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid()); block.process(); repository.saveChanges(); @@ -644,12 +628,12 @@ public class TransactionTests { // Check sender's balance BigDecimal expectedBalance = initialSenderBalance.subtract(fee); BigDecimal actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance(); - assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Sender's new balance incorrect"); + assertTrue("Sender's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); // Fee should be in generator's balance expectedBalance = initialGeneratorBalance.add(fee); actualBalance = accountRepository.getBalance(generator.getAddress(), Asset.QORA).getBalance(); - assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Generator's new balance incorrect"); + assertTrue("Generator's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); // Check we now have an assetId Long assetId = issueAssetTransactionData.getAssetId(); @@ -673,11 +657,11 @@ public class TransactionTests { // Check sender's balance actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance(); - assertTrue(initialSenderBalance.compareTo(actualBalance) == 0, "Sender's reverted balance incorrect"); + assertTrue("Sender's reverted balance incorrect", initialSenderBalance.compareTo(actualBalance) == 0); // Check generator's balance actualBalance = accountRepository.getBalance(generator.getAddress(), Asset.QORA).getBalance(); - assertTrue(initialGeneratorBalance.compareTo(actualBalance) == 0, "Generator's reverted balance incorrect"); + assertTrue("Generator's reverted balance incorrect", initialGeneratorBalance.compareTo(actualBalance) == 0); // Check asset no longer exists assertFalse(assetRepo.assetExists(assetId)); @@ -725,8 +709,8 @@ public class TransactionTests { block.addTransaction(transferAssetTransactionData); block.sign(); - assertTrue(block.isSignatureValid(), "Block signatures invalid"); - assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid"); + assertTrue("Block signatures invalid", block.isSignatureValid()); + assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid()); block.process(); repository.saveChanges(); @@ -734,12 +718,12 @@ public class TransactionTests { // Check sender's balance BigDecimal expectedBalance = originalSenderBalance.subtract(fee); BigDecimal actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance(); - assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Sender's new balance incorrect"); + assertTrue("Sender's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); // Fee should be in generator's balance expectedBalance = originalGeneratorBalance.add(fee); actualBalance = accountRepository.getBalance(generator.getAddress(), Asset.QORA).getBalance(); - assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Generator's new balance incorrect"); + assertTrue("Generator's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); // Check asset balances BigDecimal actualSenderAssetBalance = sender.getConfirmedBalance(assetId); @@ -757,11 +741,11 @@ public class TransactionTests { // Check sender's balance actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance(); - assertTrue(originalSenderBalance.compareTo(actualBalance) == 0, "Sender's reverted balance incorrect"); + assertTrue("Sender's reverted balance incorrect", originalSenderBalance.compareTo(actualBalance) == 0); // Check generator's balance actualBalance = accountRepository.getBalance(generator.getAddress(), Asset.QORA).getBalance(); - assertTrue(originalGeneratorBalance.compareTo(actualBalance) == 0, "Generator's reverted balance incorrect"); + assertTrue("Generator's reverted balance incorrect", originalGeneratorBalance.compareTo(actualBalance) == 0); // Check asset balances actualSenderAssetBalance = sender.getConfirmedBalance(assetId); @@ -817,8 +801,8 @@ public class TransactionTests { BigDecimal fee = BigDecimal.ONE; long timestamp = parentBlockData.getTimestamp() + 1_000; - CreateAssetOrderTransactionData createOrderTransactionData = new CreateAssetOrderTransactionData(timestamp, Group.NO_GROUP, buyersReference, buyer.getPublicKey(), haveAssetId, - wantAssetId, amount, price, fee); + CreateAssetOrderTransactionData createOrderTransactionData = new CreateAssetOrderTransactionData(timestamp, Group.NO_GROUP, buyersReference, + buyer.getPublicKey(), haveAssetId, wantAssetId, amount, price, fee); Transaction createOrderTransaction = new CreateAssetOrderTransaction(this.repository, createOrderTransactionData); createOrderTransaction.sign(buyer); assertTrue(createOrderTransaction.isSignatureValid()); @@ -829,8 +813,8 @@ public class TransactionTests { block.addTransaction(createOrderTransactionData); block.sign(); - assertTrue(block.isSignatureValid(), "Block signatures invalid"); - assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid"); + assertTrue("Block signatures invalid", block.isSignatureValid()); + assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid()); block.process(); repository.saveChanges(); @@ -898,7 +882,8 @@ public class TransactionTests { BigDecimal fee = BigDecimal.ONE; long timestamp = parentBlockData.getTimestamp() + 1_000; byte[] buyersReference = buyer.getLastReference(); - CancelAssetOrderTransactionData cancelOrderTransactionData = new CancelAssetOrderTransactionData(timestamp, Group.NO_GROUP, buyersReference, buyer.getPublicKey(), orderId, fee); + CancelAssetOrderTransactionData cancelOrderTransactionData = new CancelAssetOrderTransactionData(timestamp, Group.NO_GROUP, buyersReference, + buyer.getPublicKey(), orderId, fee); Transaction cancelOrderTransaction = new CancelAssetOrderTransaction(this.repository, cancelOrderTransactionData); cancelOrderTransaction.sign(buyer); @@ -910,8 +895,8 @@ public class TransactionTests { block.addTransaction(cancelOrderTransactionData); block.sign(); - assertTrue(block.isSignatureValid(), "Block signatures invalid"); - assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid"); + assertTrue("Block signatures invalid", block.isSignatureValid()); + assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid()); block.process(); repository.saveChanges(); @@ -973,8 +958,8 @@ public class TransactionTests { long timestamp = parentBlockData.getTimestamp() + 1_000; BigDecimal senderPreTradeWantBalance = sender.getConfirmedBalance(wantAssetId); - CreateAssetOrderTransactionData createOrderTransactionData = new CreateAssetOrderTransactionData(timestamp, Group.NO_GROUP, reference, sender.getPublicKey(), haveAssetId, - wantAssetId, amount, price, fee); + CreateAssetOrderTransactionData createOrderTransactionData = new CreateAssetOrderTransactionData(timestamp, Group.NO_GROUP, reference, + sender.getPublicKey(), haveAssetId, wantAssetId, amount, price, fee); Transaction createOrderTransaction = new CreateAssetOrderTransaction(this.repository, createOrderTransactionData); createOrderTransaction.sign(sender); assertTrue(createOrderTransaction.isSignatureValid()); @@ -985,8 +970,8 @@ public class TransactionTests { block.addTransaction(createOrderTransactionData); block.sign(); - assertTrue(block.isSignatureValid(), "Block signatures invalid"); - assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid"); + assertTrue("Block signatures invalid", block.isSignatureValid()); + assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid()); block.process(); repository.saveChanges(); @@ -999,7 +984,7 @@ public class TransactionTests { // Check order has trades List trades = assetRepo.getOrdersTrades(orderId); assertNotNull(trades); - assertEquals(1, trades.size(), "Trade didn't happen"); + assertEquals("Trade didn't happen", 1, trades.size()); TradeData tradeData = trades.get(0); // Check trade has correct values @@ -1082,7 +1067,8 @@ public class TransactionTests { payments.add(paymentData); } - MultiPaymentTransactionData multiPaymentTransactionData = new MultiPaymentTransactionData(timestamp, Group.NO_GROUP, reference, sender.getPublicKey(), payments, fee); + MultiPaymentTransactionData multiPaymentTransactionData = new MultiPaymentTransactionData(timestamp, Group.NO_GROUP, reference, sender.getPublicKey(), + payments, fee); Transaction multiPaymentTransaction = new MultiPaymentTransaction(repository, multiPaymentTransactionData); multiPaymentTransaction.sign(sender); @@ -1094,20 +1080,20 @@ public class TransactionTests { block.addTransaction(multiPaymentTransactionData); block.sign(); - assertTrue(block.isSignatureValid(), "Block signatures invalid"); - assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid"); + assertTrue("Block signatures invalid", block.isSignatureValid()); + assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid()); block.process(); repository.saveChanges(); // Check sender's balance BigDecimal actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance(); - assertTrue(expectedSenderBalance.compareTo(actualBalance) == 0, "Sender's new balance incorrect"); + assertTrue("Sender's new balance incorrect", expectedSenderBalance.compareTo(actualBalance) == 0); // Fee should be in generator's balance BigDecimal expectedBalance = initialGeneratorBalance.add(fee); actualBalance = accountRepository.getBalance(generator.getAddress(), Asset.QORA).getBalance(); - assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Generator's new balance incorrect"); + assertTrue("Generator's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); // Check recipients for (int i = 0; i < payments.size(); ++i) { @@ -1115,12 +1101,12 @@ public class TransactionTests { Account recipient = new Account(this.repository, paymentData.getRecipient()); byte[] recipientsReference = recipient.getLastReference(); - assertTrue(Arrays.equals(multiPaymentTransaction.getTransactionData().getSignature(), recipientsReference), "Recipient's new reference incorrect"); + assertTrue("Recipient's new reference incorrect", Arrays.equals(multiPaymentTransaction.getTransactionData().getSignature(), recipientsReference)); // Amount should be in recipient's balance expectedBalance = paymentData.getAmount(); actualBalance = accountRepository.getBalance(recipient.getAddress(), Asset.QORA).getBalance(); - assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Recipient's new balance incorrect"); + assertTrue("Recipient's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); } @@ -1130,11 +1116,11 @@ public class TransactionTests { // Check sender's balance actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance(); - assertTrue(initialSenderBalance.compareTo(actualBalance) == 0, "Sender's reverted balance incorrect"); + assertTrue("Sender's reverted balance incorrect", initialSenderBalance.compareTo(actualBalance) == 0); // Check generator's balance actualBalance = accountRepository.getBalance(generator.getAddress(), Asset.QORA).getBalance(); - assertTrue(initialGeneratorBalance.compareTo(actualBalance) == 0, "Generator's new balance incorrect"); + assertTrue("Generator's new balance incorrect", initialGeneratorBalance.compareTo(actualBalance) == 0); } @Test @@ -1164,8 +1150,8 @@ public class TransactionTests { block.addTransaction(messageTransactionData); block.sign(); - assertTrue(block.isSignatureValid(), "Block signatures invalid"); - assertEquals(Block.ValidationResult.OK, block.isValid(), "Block is invalid"); + assertTrue("Block signatures invalid", block.isSignatureValid()); + assertEquals("Block is invalid", Block.ValidationResult.OK, block.isValid()); block.process(); repository.saveChanges(); @@ -1173,17 +1159,17 @@ public class TransactionTests { // Check sender's balance BigDecimal expectedBalance = initialSenderBalance.subtract(amount).subtract(fee); BigDecimal actualBalance = accountRepository.getBalance(sender.getAddress(), Asset.QORA).getBalance(); - assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Sender's new balance incorrect"); + assertTrue("Sender's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); // Fee should be in generator's balance expectedBalance = initialGeneratorBalance.add(fee); actualBalance = accountRepository.getBalance(generator.getAddress(), Asset.QORA).getBalance(); - assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Generator's new balance incorrect"); + assertTrue("Generator's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); // Amount should be in recipient's balance expectedBalance = amount; actualBalance = accountRepository.getBalance(recipient.getAddress(), Asset.QORA).getBalance(); - assertTrue(expectedBalance.compareTo(actualBalance) == 0, "Recipient's new balance incorrect"); + assertTrue("Recipient's new balance incorrect", expectedBalance.compareTo(actualBalance) == 0); } } \ No newline at end of file diff --git a/src/test/java/org/qora/test/utils/AssertExtensions.java b/src/test/java/org/qora/test/utils/AssertExtensions.java deleted file mode 100644 index 747b001f..00000000 --- a/src/test/java/org/qora/test/utils/AssertExtensions.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.qora.test.utils; - -import java.util.Collection; - -import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder; -import static org.hamcrest.MatcherAssert.assertThat; - -public class AssertExtensions { - - public static void assertItemsEqual(Collection expected, Iterable actual) { - assertItemsEqual(expected, actual, (String) null); - } - - public static void assertItemsEqual(Collection expected, Iterable actual, String message) { - assertThat(message, actual, containsInAnyOrder(expected.toArray())); - } - -} diff --git a/src/test/resources/test-settings.json b/src/test/resources/test-settings.json new file mode 100644 index 00000000..4fe80523 --- /dev/null +++ b/src/test/resources/test-settings.json @@ -0,0 +1,6 @@ +{ + "restrictedApi": false, + "blockchainConfig": "src/test/resources/test-v2qorachain.json", + "wipeUnconfirmedOnStart": false, + "minPeers": 0 +} diff --git a/src/test/resources/test-v2qorachain.json b/src/test/resources/test-v2qorachain.json new file mode 100644 index 00000000..c56b090b --- /dev/null +++ b/src/test/resources/test-v2qorachain.json @@ -0,0 +1,31 @@ +{ + "isTestNet": true, + "maxBalance": "10000000000", + "blockDifficultyInterval": 10, + "minBlockTime": 30000, + "maxBlockTime": 60000, + "blockTimestampMargin": 500, + "maxBytesPerUnitFee": 1024, + "unitFee": "0.1", + "requireGroupForApproval": true, + "defaultGroupId": 2, + "genesisInfo": { + "version": 4, + "timestamp": 0, + "generatingBalance": "10000000", + "transactions": [ + { "type": "ISSUE_ASSET", "owner": "QcFmNxSArv5tWEzCtTKb2Lqc5QkKuQ7RNs", "assetName": "QORA", "description": "QORA native coin", "quantity": 10000000000, "isDivisible": true, "fee": 0, "reference": "3Verk6ZKBJc3WTTVfxFC9icSjKdM8b92eeJEpJP8qNizG4ZszNFq8wdDYdSjJXq2iogDFR1njyhsBdVpbvDfjzU7" }, + { "type": "GENESIS", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "amount": "9876543210.12345678", "fee": 0 }, + { "type": "CREATE_GROUP", "creatorPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "owner": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "groupName": "dev-group", "description": "developer group", "isOpen": false, "approvalThreshold": "PCT100", "minimumBlockDelay": 0, "maximumBlockDelay": 1440 } + ] + }, + "featureTriggers": { + "messageHeight": 0, + "atHeight": 0, + "assetsTimestamp": 0, + "votingTimestamp": 0, + "arbitraryTimestamp": 0, + "powfixTimestamp": 0, + "v2Timestamp": 0 + } +}