Proper JSON unmarshalling for settings, blockchain config, genesis block

GenesisBlock (v4) now supports various transaction types (issue-asset, etc.)
 with generated signatures (like genesis transaction signature) and
 missing references inserted.

JUnit reverted back to v4 for Eclipse support (for now).
This commit is contained in:
catbref
2019-02-25 13:31:05 +00:00
parent 86a35c3b71
commit 16c1b13ab2
43 changed files with 988 additions and 827 deletions

View File

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

View File

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

View File

@@ -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()) {

View File

@@ -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()) {

View File

@@ -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()) {

View File

@@ -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()) {

View File

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

View File

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

View File

@@ -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())

View File

@@ -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<String, Map<FeatureValueType, Long>> featureTriggers;
@XmlJavaTypeAdapter(StringLongMapXmlAdapter.class)
private Map<String, Long> 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<FeatureValueType, Long> 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<String, Map<FeatureValueType, Long>> 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();

View File

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

View File

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

View File

@@ -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() {
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<String> apiAllowed = new ArrayList<String>(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<String> 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<String>();
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<String> 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<String> 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");
}
}
}

View File

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

View File

@@ -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.
* <p>
* 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());

View File

@@ -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<StringLongMapXmlAdapter.StringLongMap, Map<String, Long>> {
public static class StringLongMap {
@XmlVariableNode("key")
List<MapEntry> entries = new ArrayList<MapEntry>();
}
public static class MapEntry {
@XmlTransient
public String key;
@XmlValue
public Long value;
}
@Override
public Map<String, Long> unmarshal(StringLongMap stringLongMap) throws Exception {
Map<String, Long> map = new HashMap<>(stringLongMap.entries.size());
for (MapEntry entry : stringLongMap.entries)
map.put(entry.key, entry.value);
return map;
}
@Override
public StringLongMap marshal(Map<String, Long> map) throws Exception {
StringLongMap output = new StringLongMap();
for (Entry<String, Long> entry : map.entrySet()) {
MapEntry mapEntry = new MapEntry();
mapEntry.key = entry.getKey();
mapEntry.value = entry.getValue();
output.entries.add(mapEntry);
}
return output;
}
}