Compare commits

...

14 Commits

Author SHA1 Message Date
catbref
2df045396d Bump to v1.2.3 2020-07-28 11:27:21 +01:00
catbref
6c182a3567 Allow minting accounts to be removed from node using public key as well as private key 2020-07-28 10:45:06 +01:00
catbref
340d6dfc8d Add websocket error handler support 2020-07-27 10:16:21 +01:00
catbref
eb27b0d3e2 Blocks websocket now returns simpler block info 2020-07-24 14:52:14 +01:00
catbref
7377893050 WebSocket improvements, inc. bump Jetty to v9.4.29-20200521
Various issues in Jetty v9.4.22 (and some later versions too)
cause websockets to use up all available threads.

Bumped Jetty to v9.4.29 to resolve some of these issues.

Changed some Qortal-side websocket code to minimize
locking on websocket notifiers. Websocket messages now
sent async, although the returned Futures are discarded,
as it's up to the remote end to consume fast enough.

Changed Controller to only request a SysTray update before
synchronization if there's a chance node might change height.
Similarly, Controller only requests SysTray update after
synchronization if chain tip has actually changed.
Both of the above together should reduce the number of
messages sent out via the admin status websockets.
2020-07-24 10:34:42 +01:00
catbref
fb2c2b1d09 Added API call GET /blocks/summaries
Returns summary info about a range of blocks.

(Not to be confused with network-related BlockSummaries)
2020-07-20 13:05:43 +01:00
catbref
6f2dd6c8d0 Added some more useful tools/scripts, mostly for Linux-based curious node owners 2020-07-17 12:22:48 +01:00
catbref
4cc0e7845f Add instructions and files used to build installers for Windows
For this commit, the included .aip file, and qortal.jar, match
what was used to produce the installer for release v1.2.2.

In a future commit, maybe remove qortal.jar as it is only included
here to illustrate current location in build tree.

Updates to .aip file could be, and maybe even should be, committed.

This build toolchain uses AdvancedInstaller v16 or better but
may require an (expensive) enterprise licence. It is possible
to obtain an 'open source'-use free licence from AdvancedInstaller
by contacting them directly. However this may result in restricted
functionality with AdvancedInstaller and some installer features,
e.g. multi-language support, may have be to removed.
2020-07-17 12:08:25 +01:00
catbref
275146fb55 Return REWARD_SHARE_UNKNOWN when trying to cancel non-existent reward-share 2020-07-14 09:53:14 +01:00
catbref
d81729d9f7 Bump to v1.2.2 2020-07-03 12:14:31 +01:00
catbref
e74a249388 Collate network PoW computes into a fixed-sized pool, with dead peer detection.
Also added Named/DaemonThreadFactory classes.

Network EPC now uses NamedThreadFactory for easier debugging.

Added settings field "networkPoWComputePoolSize", default 2, which
seems to work with both low-power ARM boards and high-power desktops.
2020-07-03 09:31:46 +01:00
catbref
d8c5e557d8 Update uiLocalServers, autoUpdateRepos and bump to v1.2.1 2020-06-30 15:41:53 +01:00
catbref
984e8b5227 Network optimizations: if we're not up to date then don't request, or send, unconfirmed transaction lists 2020-06-30 14:26:12 +01:00
catbref
469bf2a63e Improve inbound peer handshaking
If a node accepts a connection from an inbound peer
then remote peer will send RESPONSE first
and local node would previously change handshaking state
to COMPLETED while computing their own RESPONSE.

This meant that the local node would sometimes also start
sending post-handshake messages to the remote peer,
e.g. TRANSACTION_SIGNATURES.

Remote peer is only expecting a RESPONSE message, so would
close connection.

So we introduce an extra handshaking state "RESPONDING" for use
by local node while they compute RESPONSE in a separate thread.
Once the RESPONSE has been sent, local node moves to COMPLETED
state and called onHandshakeCompleted() as per usual.

Note that the code path when connecting outbound to a remote peer
is not changed, and the RESPONDING state is not used.

Also in this commit:

Network.onPeerReady now bypasses call to onMessage and instead
calls onHandshakingMessage() directly to avoid race condition
where peer's handshake status could change between
onPeerReady's caller and onMessage() calling peer.getHandshakeStatus()
2020-06-30 13:37:14 +01:00
37 changed files with 2427 additions and 77 deletions

View File

@@ -0,0 +1,3 @@
{
"apiDocumentationEnabled": true
}

View File

@@ -0,0 +1,70 @@
rootLogger.level = info
# On Windows, uncomment next line to set dirname:
# property.dirname = ${sys:user.home}\\AppData\\Local\\qortal\\
property.filename = ${sys:log4j2.filenameTemplate:-log.txt}
rootLogger.appenderRef.console.ref = stdout
rootLogger.appenderRef.rolling.ref = FILE
# Suppress extraneous bitcoinj library output
logger.bitcoinj.name = org.bitcoinj
logger.bitcoinj.level = error
# Override HSQLDB logging level to "warn" as too much is logged at "info"
logger.hsqldb.name = hsqldb.db
logger.hsqldb.level = warn
# Support optional, per-session HSQLDB debugging
logger.hsqldbRepository.name = org.qortal.repository.hsqldb
logger.hsqldbRepository.level = debug
# Suppress extraneous Jersey warning
logger.jerseyInject.name = org.glassfish.jersey.internal.inject.Providers
logger.jerseyInject.level = off
# Suppress extraneous Jersey EOF 'errors' (actually remote peers disconnecting early)
logger.jerseyEOF.name = org.glassfish.jersey.server.internal
logger.jerseyEOF.level = off
# Suppress extraneous Jetty entries
# 2019-02-14 11:46:27 INFO ContextHandler:851 - Started o.e.j.s.ServletContextHandler@6949e948{/,null,AVAILABLE}
# 2019-02-14 11:46:27 INFO AbstractConnector:289 - Started ServerConnector@50ad322b{HTTP/1.1,[http/1.1]}{0.0.0.0:9085}
# 2019-02-14 11:46:27 INFO Server:374 - jetty-9.4.11.v20180605; built: 2018-06-05T18:24:03.829Z; git: d5fc0523cfa96bfebfbda19606cad384d772f04c; jvm 1.8.0_181-b13
# 2019-02-14 11:46:27 INFO Server:411 - Started @2539ms
logger.jetty.name = org.eclipse.jetty
logger.jetty.level = warn
# Even more extraneous Jetty output
# 2019-01-26 02:18:10 WARN ResourceService:718 - java.util.concurrent.TimeoutException: Idle timeout expired: 30000/30000 ms
logger.jettyRS.name = org.eclipse.jetty.server.ResourceService
logger.jettyRS.level = error
# Suppress extraneous slf4j entries
# 2019-02-14 11:46:27 INFO log:193 - Logging initialized @1636ms to org.eclipse.jetty.util.log.Slf4jLog
logger.slf4j.name = org.slf4j
logger.slf4j.level = warn
# Suppress extraneous Reflections entry
# 2019-02-27 10:45:25 WARN Reflections:179 - given scan urls are empty. set urls in the configuration
logger.orgReflections.name = org.reflections.Reflections
logger.orgReflections.level = off
logger.sunReflections.name = sun.reflect.Reflection
logger.sunReflections.level = off
appender.console.type = Console
appender.console.name = stdout
appender.console.layout.type = PatternLayout
appender.console.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
appender.console.filter.threshold.type = ThresholdFilter
appender.console.filter.threshold.level = error
appender.rolling.type = RollingFile
appender.rolling.name = FILE
appender.rolling.layout.type = PatternLayout
appender.rolling.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
appender.rolling.filePattern = ${dirname:-}${filename}.%i
appender.rolling.policy.type = SizeBasedTriggeringPolicy
appender.rolling.policy.size = 4MB
# Set the immediate flush to true (default)
# appender.rolling.immediateFlush = true
# Set the append to true (default), should not overwrite
# appender.rolling.append=true

View File

@@ -0,0 +1,33 @@
@echo off
:: BatchGotAdmin
:-------------------------------------
REM --> Check for permissions
>nul 2>&1 "%SYSTEMROOT%\system32\cacls.exe" "%SYSTEMROOT%\system32\config\system"
REM --> If error flag set, we do not have admin.
if '%errorlevel%' NEQ '0' (
echo Requesting administrative privileges...
goto UACPrompt
) else ( goto gotAdmin )
:UACPrompt
echo Set UAC = CreateObject^("Shell.Application"^) > "%temp%\getadmin.vbs"
echo UAC.ShellExecute "%~s0", "", "", "runas", 1 >> "%temp%\getadmin.vbs"
"%temp%\getadmin.vbs"
exit /B
:gotAdmin
if exist "%temp%\getadmin.vbs" ( del "%temp%\getadmin.vbs" )
pushd "%CD%"
CD /D "%~dp0"
:--------------------------------------
net stop "Windows Time"
w32tm /config "/manualpeerlist:pool.ntp.org 0.pool.ntp.org 1.pool.ntp.org 2.pool.ntp.org 3.pool.ntp.org cn.pool.ntp.org 0.cn.pool.ntp.org 1.cn.pool.ntp.org 2.cn.pool.ntp.org 3.cn.pool.ntp.org"
net start "Windows Time"
sc config w32time start= auto

Binary file not shown.

1268
WindowsInstaller/Qortal.aip Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,26 @@
# Windows installer
## Prerequisites
* AdvancedInstaller v16 or better, and enterprise licence if translations are required
* Installed AdoptOpenJDK v11 64bit, full JDK *not* JRE
## General build instructions
If this is your first time opening the `qortal.aip` file then you might need to adjust
configured paths, or create a dummy `D:` drive with the expected layout.
Typical build procedure:
* Overwrite the `qortal.jar` file in `Install-Files\`
* Open AdvancedInstaller with qortal.aip file
* If releasing a new version, change version number in:
+ "Product Information" side menu
+ "Product Details" side menu entry
+ "Product Details" tab in "Product Details" pane
+ "Product Version" entry box
* Click away to a different side menu entry, e.g. "Resources" -> "Files and Folders"
* You should be prompted whether to generate a new product key, click "Generate New"
* Click "Build" button
* New EXE should be generated in `Qortal-SetupFiles\` folder with correct version number

97
WindowsInstaller/dictionary.ail Executable file
View File

@@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<DICTIONARY type="multilanguage">
<!-- Control table -->
<ENTRY id="Control.Text.CustomizeDataPathDlg#Description">
<STRING lang="en" value="Do you want to store the blockchain, and other data, in a specific folder?"/>
<STRING lang="zh" value="你想把区块链数据存放在一个特定的文件夹吗?"/>
<STRING lang="zh_TW" value="你想把区块链数据存放在一个特定的文件夹吗?"/>
</ENTRY>
<ENTRY id="Control.Text.CustomizeDataPathDlg#Text">
<STRING lang="en" value="Select one of the options below, then click &quot;Next&quot;."/>
<STRING lang="zh" value="请选择,然后“下一步”"/>
<STRING lang="zh_TW" value="请选择,然后“下一步”"/>
</ENTRY>
<ENTRY id="Control.Text.CustomizeDataPathDlg#Title">
<STRING lang="en" value="Choose Custom Data Storage Folder?"/>
<STRING lang="zh" value="选择数据保存的文件夹?"/>
<STRING lang="zh_TW" value="选择数据保存的文件夹?"/>
</ENTRY>
<ENTRY id="Control.Text.CustomizeDbDlg#Description">
<STRING lang="en" value="Do you want to store the blockchain, and other data, in a specific folder?"/>
<STRING lang="zh" value="Do you want to store the blockchain, and other data, in a specific folder?"/>
<STRING lang="zh_TW" value="Do you want to store the blockchain, and other data, in a specific folder?"/>
</ENTRY>
<ENTRY id="Control.Text.CustomizeDbDlg#Title">
<STRING lang="en" value="Choose Custom Data Storage Folder?"/>
<STRING lang="zh" value="Choose Custom Data Storage Folder?"/>
<STRING lang="zh_TW" value="Choose Custom Data Storage Folder?"/>
</ENTRY>
<ENTRY id="Control.Text.DataFolderDlg#Description">
<STRING lang="en" value="This is the folder where the blockchain, and other data, will be stored."/>
<STRING lang="zh" value="这里是区块链及其它数据存放的文件夹"/>
<STRING lang="zh_TW" value="这里是区块链及其它数据存放的文件夹"/>
</ENTRY>
<ENTRY id="Control.Text.DataFolderDlg#Text">
<STRING lang="en" value="To store data in this folder, click &quot;[Text_Next]&quot;. To store data in a different folder, enter it below or click &quot;Browse&quot;."/>
<STRING lang="zh" value="如果存放在这个文件夹,点 “下一步”。如果存放在其它位置,请选择“浏览”。"/>
<STRING lang="zh_TW" value="如果存放在这个文件夹,点 “下一步”。如果存放在其它位置,请选择“浏览”。"/>
</ENTRY>
<ENTRY id="Control.Text.DataFolderDlg#Title">
<STRING lang="en" value="Select Data Storage Folder"/>
<STRING lang="zh" value="请选择文件存储地方"/>
<STRING lang="zh_TW" value="请选择文件存储地方"/>
</ENTRY>
<ENTRY id="Control.Text.DbFolderDlg#Description">
<STRING lang="en" value="This is the folder where the blockchain, and other data, will be stored."/>
<STRING lang="zh" value="This is the folder where the blockchain, and other data, will be stored."/>
<STRING lang="zh_TW" value="This is the folder where the blockchain, and other data, will be stored."/>
</ENTRY>
<ENTRY id="Control.Text.DbFolderDlg#Title">
<STRING lang="en" value="Select Data Storage Folder"/>
<STRING lang="zh" value="请选择文件存储地方"/>
<STRING lang="zh_TW" value="请选择文件存储地方"/>
</ENTRY>
<ENTRY id="Control.Text.NTPDialog#Description">
<STRING lang="en" value="Reconfigure Windows for more accurate time?"/>
<STRING lang="zh" value="重新配置Windows以获得更准确的时间"/>
</ENTRY>
<ENTRY id="Control.Text.NTPDialog#Text_1">
<STRING lang="en" value="An accurate Windows clock is required to connect to the [ProductName] network and make transactions."/>
<STRING lang="zh" value="需要准确的Windows时钟才能连接到[ProductName]网络并进行交易。"/>
</ENTRY>
<ENTRY id="Control.Text.NTPDialog#Text_2">
<STRING lang="en" value="Select one of the options below, then click &quot;Next&quot;."/>
<STRING lang="zh" value="请选择,然后“下一步”"/>
</ENTRY>
<ENTRY id="Control.Text.NTPDialog#Text_3">
<STRING lang="en" value="Your computer&apos;s clock needs to be accurate to within 0.5 seconds."/>
<STRING lang="zh" value="您的计算机时钟需要准确到0.5秒内。"/>
</ENTRY>
<ENTRY id="Control.Text.NTPDialog#Title">
<STRING lang="en" value="Windows clock accuracy"/>
<STRING lang="zh" value="Windows 时钟精度"/>
</ENTRY>
<ENTRY id="Control.Text.VerifyRemoveDlg#RemoveBlockchainCheckbox">
<STRING lang="en" value="Remove downloaded blockchain and other data"/>
<STRING lang="zh" value="删除您下载的区块链"/>
</ENTRY>
<!-- RadioButton table -->
<ENTRY id="RadioButton.Text.CUSTOM_DB_BOOL#choose">
<STRING lang="en" value="Choose custom data storage folder..."/>
<STRING lang="zh" value="选择特定的文件夹存储"/>
<STRING lang="zh_TW" value="选择特定的文件夹存储"/>
</ENTRY>
<ENTRY id="RadioButton.Text.CUSTOM_DB_BOOL#default">
<STRING lang="en" value="Use default location "/>
<STRING lang="zh" value="使用默认存储地点"/>
<STRING lang="zh_TW" value="使用默认存储地点"/>
</ENTRY>
<ENTRY id="RadioButton.Text.RECONFIG_NTP#1">
<STRING lang="en" value="Yes, configure Windows to use internet time servers (Recommended)"/>
<STRING lang="zh" value="是将Windows配置为使用多个Internet时间服务器 (推荐的)"/>
</ENTRY>
<ENTRY id="RadioButton.Text.RECONFIG_NTP#2">
<STRING lang="en" value="No, I will manage clock accuracy myself"/>
<STRING lang="zh" value="不,我会自己管理时钟精度。"/>
</ENTRY>
</DICTIONARY>

BIN
WindowsInstaller/qortal.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

View File

@@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.qortal</groupId>
<artifactId>qortal</artifactId>
<version>1.2.0</version>
<version>1.2.3</version>
<packaging>jar</packaging>
<properties>
<bitcoinj.version>0.15.5</bitcoinj.version>
@@ -17,7 +17,7 @@
<hsqldb.version>2.5.0-fixed</hsqldb.version>
<hsqldb-sqltool.version>2.5.0</hsqldb-sqltool.version>
<jersey.version>2.29.1</jersey.version>
<jetty.version>9.4.22.v20191022</jetty.version>
<jetty.version>9.4.29.v20200521</jetty.version>
<log4j.version>2.12.1</log4j.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<slf4j.version>1.7.12</slf4j.version>

View File

@@ -3,6 +3,7 @@ package org.qortal.api;
import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -17,7 +18,7 @@ public class ApiErrorHandler extends ErrorHandler {
private static final Logger LOGGER = LogManager.getLogger(ApiErrorHandler.class);
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException {
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
if (Settings.getInstance().isApiLoggingEnabled()) {
String requestURI = request.getRequestURI();

View File

@@ -18,9 +18,9 @@ import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.rewrite.handler.RedirectPatternRule;
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
import org.eclipse.jetty.server.CustomRequestLog;
import org.eclipse.jetty.server.DetectorConnectionFactory;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.OptionalSslConnectionFactory;
import org.eclipse.jetty.server.RequestLog;
import org.eclipse.jetty.server.RequestLogWriter;
import org.eclipse.jetty.server.SecureRequestCustomizer;
@@ -113,8 +113,7 @@ public class ApiService {
SslConnectionFactory sslConnectionFactory = new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString());
ServerConnector portUnifiedConnector = new ServerConnector(this.server,
new OptionalSslConnectionFactory(sslConnectionFactory, HttpVersion.HTTP_1_1.asString()),
sslConnectionFactory,
new DetectorConnectionFactory(sslConnectionFactory),
httpConnectionFactory);
portUnifiedConnector.setHost(Settings.getInstance().getBindAddress());
portUnifiedConnector.setPort(Settings.getInstance().getApiPort());

View File

@@ -0,0 +1,71 @@
package org.qortal.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import org.qortal.data.account.RewardShareData;
import org.qortal.data.block.BlockData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
@XmlAccessorType(XmlAccessType.FIELD)
public class BlockInfo {
private byte[] signature;
private int height;
private long timestamp;
private int transactionCount;
private String minterAddress;
protected BlockInfo() {
/* For JAXB */
}
public BlockInfo(byte[] signature, int height, long timestamp, int transactionCount, String minterAddress) {
this.signature = signature;
this.height = height;
this.timestamp = timestamp;
this.transactionCount = transactionCount;
this.minterAddress = minterAddress;
}
public BlockInfo(BlockData blockData) {
// Convert BlockData to BlockInfo, using additional data
this.minterAddress = "unknown?";
try (final Repository repository = RepositoryManager.getRepository()) {
RewardShareData rewardShareData = repository.getAccountRepository().getRewardShare(blockData.getMinterPublicKey());
if (rewardShareData != null)
this.minterAddress = rewardShareData.getMintingAccount();
} catch (DataException e) {
// We'll carry on with placeholder minterAddress then...
}
this.signature = blockData.getSignature();
this.height = blockData.getHeight();
this.timestamp = blockData.getTimestamp();
this.transactionCount = blockData.getTransactionCount();
}
public byte[] getSignature() {
return this.signature;
}
public int getHeight() {
return this.height;
}
public long getTimestamp() {
return this.timestamp;
}
public int getTransactionCount() {
return this.transactionCount;
}
public String getMinterAddress() {
return this.minterAddress;
}
}

View File

@@ -302,13 +302,13 @@ public class AdminResource {
@DELETE
@Path("/mintingaccounts")
@Operation(
summary = "Remove account/reward-share from use by BlockMinter, using private key",
summary = "Remove account/reward-share from use by BlockMinter, using public or private key",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string", example = "private key"
type = "string", example = "public or private key"
)
)
),
@@ -319,13 +319,13 @@ public class AdminResource {
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.REPOSITORY_ISSUE})
public String deleteMintingAccount(String seed58) {
public String deleteMintingAccount(String key58) {
Security.checkApiCallAllowed(request);
try (final Repository repository = RepositoryManager.getRepository()) {
byte[] seed = Base58.decode(seed58.trim());
byte[] key = Base58.decode(key58.trim());
if (repository.getAccountRepository().delete(seed) == 0)
if (repository.getAccountRepository().delete(key) == 0)
return "false";
repository.saveChanges();

View File

@@ -23,6 +23,7 @@ import javax.ws.rs.core.MediaType;
import org.qortal.api.ApiError;
import org.qortal.api.ApiErrors;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.model.BlockInfo;
import org.qortal.api.model.BlockSignerSummary;
import org.qortal.crypto.Crypto;
import org.qortal.data.account.AccountData;
@@ -480,4 +481,44 @@ public class BlocksResource {
}
}
@GET
@Path("/summaries")
@Operation(
summary = "Fetch only summary info about a range of blocks",
description = "Specify up to 2 out 3 of: start, end and count. If neither start nor end are specified, then end is assumed to be latest block. Where necessary, count is assumed to be 50.",
responses = {
@ApiResponse(
description = "blocks",
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = BlockInfo.class
)
)
)
)
}
)
@ApiErrors({
ApiError.REPOSITORY_ISSUE
})
public List<BlockInfo> getBlockRange(
@QueryParam("start") Integer startHeight,
@QueryParam("end") Integer endHeight,
@Parameter(ref = "count") @QueryParam("count") Integer count) {
// Check up to 2 out of 3 params
if (startHeight != null && endHeight != null && count != null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
// Check values
if ((startHeight != null && startHeight < 1) || (endHeight != null && endHeight < 1) || (count != null && count < 1))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
try (final Repository repository = RepositoryManager.getRepository()) {
return repository.getBlockRepository().getBlockInfos(startHeight, endHeight, count);
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
}

View File

@@ -6,8 +6,10 @@ import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketException;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
@@ -52,6 +54,10 @@ public class ActiveChatsWebSocket extends WebSocketServlet implements ApiWebSock
ChatNotifier.getInstance().deregister(session);
}
@OnWebSocketError
public void onWebSocketError(Session session, Throwable throwable) {
}
@OnWebSocketMessage
public void onWebSocketMessage(Session session, String message) {
}
@@ -78,8 +84,8 @@ public class ActiveChatsWebSocket extends WebSocketServlet implements ApiWebSock
return;
previousOutput.set(output);
session.getRemote().sendString(output);
} catch (DataException | IOException e) {
session.getRemote().sendStringByFuture(output);
} catch (DataException | IOException | WebSocketException e) {
// No output this time?
}
}

View File

@@ -5,8 +5,10 @@ import java.io.StringWriter;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketException;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
@@ -41,6 +43,10 @@ public class AdminStatusWebSocket extends WebSocketServlet implements ApiWebSock
StatusNotifier.getInstance().deregister(session);
}
@OnWebSocketError
public void onWebSocketError(Session session, Throwable throwable) {
}
@OnWebSocketMessage
public void onWebSocketMessage(Session session, String message) {
}
@@ -59,8 +65,8 @@ public class AdminStatusWebSocket extends WebSocketServlet implements ApiWebSock
return;
previousOutput.set(output);
session.getRemote().sendString(output);
} catch (DataException | IOException e) {
session.getRemote().sendStringByFuture(output);
} catch (DataException | IOException | WebSocketException e) {
// No output this time?
}
}

View File

@@ -2,17 +2,20 @@ package org.qortal.api.websocket;
import java.io.IOException;
import java.io.StringWriter;
import java.util.List;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketException;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.qortal.api.ApiError;
import org.qortal.api.model.BlockInfo;
import org.qortal.controller.BlockNotifier;
import org.qortal.data.block.BlockData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
@@ -29,7 +32,7 @@ public class BlocksWebSocket extends WebSocketServlet implements ApiWebSocket {
@OnWebSocketConnect
public void onWebSocketConnect(Session session) {
BlockNotifier.Listener listener = blockData -> onNotify(session, blockData);
BlockNotifier.Listener listener = blockInfo -> onNotify(session, blockInfo);
BlockNotifier.getInstance().register(session, listener);
}
@@ -38,6 +41,10 @@ public class BlocksWebSocket extends WebSocketServlet implements ApiWebSocket {
BlockNotifier.getInstance().deregister(session);
}
@OnWebSocketError
public void onWebSocketError(Session session, Throwable throwable) {
}
@OnWebSocketMessage
public void onWebSocketMessage(Session session, String message) {
// We're expecting either a base58 block signature or an integer block height
@@ -53,13 +60,19 @@ public class BlocksWebSocket extends WebSocketServlet implements ApiWebSocket {
}
try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().fromSignature(signature);
if (blockData == null) {
int height = repository.getBlockRepository().getHeightFromSignature(signature);
if (height == 0) {
sendError(session, ApiError.BLOCK_UNKNOWN);
return;
}
onNotify(session, blockData);
List<BlockInfo> blockInfos = repository.getBlockRepository().getBlockInfos(height, null, 1);
if (blockInfos == null || blockInfos.isEmpty()) {
sendError(session, ApiError.BLOCK_UNKNOWN);
return;
}
onNotify(session, blockInfos.get(0));
} catch (DataException e) {
sendError(session, ApiError.REPOSITORY_ISSUE);
}
@@ -82,26 +95,26 @@ public class BlocksWebSocket extends WebSocketServlet implements ApiWebSocket {
}
try (final Repository repository = RepositoryManager.getRepository()) {
BlockData blockData = repository.getBlockRepository().fromHeight(height);
if (blockData == null) {
List<BlockInfo> blockInfos = repository.getBlockRepository().getBlockInfos(height, null, 1);
if (blockInfos == null || blockInfos.isEmpty()) {
sendError(session, ApiError.BLOCK_UNKNOWN);
return;
}
onNotify(session, blockData);
onNotify(session, blockInfos.get(0));
} catch (DataException e) {
sendError(session, ApiError.REPOSITORY_ISSUE);
}
}
private void onNotify(Session session, BlockData blockData) {
private void onNotify(Session session, BlockInfo blockInfo) {
StringWriter stringWriter = new StringWriter();
try {
this.marshall(stringWriter, blockData);
this.marshall(stringWriter, blockInfo);
session.getRemote().sendString(stringWriter.toString());
} catch (IOException e) {
session.getRemote().sendStringByFuture(stringWriter.toString());
} catch (IOException | WebSocketException e) {
// No output this time
}
}

View File

@@ -8,8 +8,10 @@ import java.util.List;
import java.util.Map;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketException;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
@@ -89,6 +91,10 @@ public class ChatMessagesWebSocket extends WebSocketServlet implements ApiWebSoc
ChatNotifier.getInstance().deregister(session);
}
@OnWebSocketError
public void onWebSocketError(Session session, Throwable throwable) {
}
@OnWebSocketMessage
public void onWebSocketMessage(Session session, String message) {
}
@@ -125,8 +131,8 @@ public class ChatMessagesWebSocket extends WebSocketServlet implements ApiWebSoc
try {
this.marshall(stringWriter, chatMessages);
session.getRemote().sendString(stringWriter.toString());
} catch (IOException e) {
session.getRemote().sendStringByFuture(stringWriter.toString());
} catch (IOException | WebSocketException e) {
// No output this time?
}
}

View File

@@ -1,9 +1,12 @@
package org.qortal.controller;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jetty.websocket.api.Session;
import org.qortal.api.model.BlockInfo;
import org.qortal.data.block.BlockData;
public class BlockNotifier {
@@ -12,7 +15,7 @@ public class BlockNotifier {
@FunctionalInterface
public interface Listener {
void notify(BlockData blockData);
void notify(BlockInfo blockInfo);
}
private Map<Session, Listener> listenersBySession = new HashMap<>();
@@ -27,17 +30,32 @@ public class BlockNotifier {
return instance;
}
public synchronized void register(Session session, Listener listener) {
this.listenersBySession.put(session, listener);
public void register(Session session, Listener listener) {
synchronized (this.listenersBySession) {
this.listenersBySession.put(session, listener);
}
}
public synchronized void deregister(Session session) {
this.listenersBySession.remove(session);
public void deregister(Session session) {
synchronized (this.listenersBySession) {
this.listenersBySession.remove(session);
}
}
public synchronized void onNewBlock(BlockData blockData) {
for (Listener listener : this.listenersBySession.values())
listener.notify(blockData);
public void onNewBlock(BlockData blockData) {
// Convert BlockData to BlockInfo
BlockInfo blockInfo = new BlockInfo(blockData);
for (Listener listener : getAllListeners())
listener.notify(blockInfo);
}
private Collection<Listener> getAllListeners() {
// Make a copy of listeners to both avoid concurrent modification
// and reduce synchronization time
synchronized (this.listenersBySession) {
return new ArrayList<>(this.listenersBySession.values());
}
}
}

View File

@@ -1,5 +1,7 @@
package org.qortal.controller;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
@@ -27,22 +29,34 @@ public class ChatNotifier {
return instance;
}
public synchronized void register(Session session, Listener listener) {
this.listenersBySession.put(session, listener);
public void register(Session session, Listener listener) {
synchronized (this.listenersBySession) {
this.listenersBySession.put(session, listener);
}
}
public synchronized void deregister(Session session) {
this.listenersBySession.remove(session);
public void deregister(Session session) {
synchronized (this.listenersBySession) {
this.listenersBySession.remove(session);
}
}
public synchronized void onNewChatTransaction(ChatTransactionData chatTransactionData) {
for (Listener listener : this.listenersBySession.values())
public void onNewChatTransaction(ChatTransactionData chatTransactionData) {
for (Listener listener : getAllListeners())
listener.notify(chatTransactionData);
}
public synchronized void onGroupMembershipChange() {
for (Listener listener : this.listenersBySession.values())
public void onGroupMembershipChange() {
for (Listener listener : getAllListeners())
listener.notify(null);
}
private Collection<Listener> getAllListeners() {
// Make a copy of listeners to both avoid concurrent modification
// and reduce synchronization time
synchronized (this.listenersBySession) {
return new ArrayList<>(this.listenersBySession.values());
}
}
}

View File

@@ -537,8 +537,11 @@ public class Controller extends Thread {
public SynchronizationResult actuallySynchronize(Peer peer, boolean force) throws InterruptedException {
syncPercent = (this.chainTip.getHeight() * 100) / peer.getChainTipData().getLastHeight();
isSynchronizing = true;
updateSysTray();
// Only update SysTray if we're potentially changing height
if (syncPercent < 100) {
isSynchronizing = true;
updateSysTray();
}
BlockData priorChainTip = this.chainTip;
@@ -584,7 +587,6 @@ public class Controller extends Thread {
break;
case OK:
requestSysTrayUpdate = true;
// fall-through...
case NOTHING_TO_DO: {
// Update our list of inferior chain tips
@@ -611,14 +613,13 @@ public class Controller extends Thread {
// Reset our cache of inferior chains
inferiorChainSignatures.clear();
// Update chain-tip, notify peers, websockets, etc.
// Update chain-tip, systray, notify peers, websockets, etc.
this.onNewBlock(newChainTip);
}
return syncResult;
} finally {
isSynchronizing = false;
requestSysTrayUpdate = true;
}
}
@@ -765,8 +766,10 @@ public class Controller extends Thread {
BlockData latestBlockData = getChainTip();
network.broadcast(peer -> network.buildHeightMessage(peer, latestBlockData));
// Send (if outbound) / Request unconfirmed transaction signatures
network.broadcast(network::buildGetUnconfirmedTransactionsMessage);
// Request unconfirmed transaction signatures, but only if we're up-to-date.
// If we're NOT up-to-date then priority is synchronizing first
if (isUpToDate())
network.broadcast(network::buildGetUnconfirmedTransactionsMessage);
}
public void onMintingPossibleChange(boolean isMintingPossible) {
@@ -1040,7 +1043,12 @@ public class Controller extends Thread {
private void onNetworkGetUnconfirmedTransactionsMessage(Peer peer, Message message) {
try (final Repository repository = RepositoryManager.getRepository()) {
List<byte[]> signatures = repository.getTransactionRepository().getUnconfirmedTransactionSignatures();
List<byte[]> signatures = Collections.emptyList();
// If we're NOT up-to-date then don't send out unconfirmed transactions
// as it's possible they are already included in a later block that we don't have.
if (isUpToDate())
signatures = repository.getTransactionRepository().getUnconfirmedTransactionSignatures();
Message transactionSignaturesMessage = new TransactionSignaturesMessage(signatures);
if (!peer.sendMessage(transactionSignaturesMessage))

View File

@@ -1,5 +1,7 @@
package org.qortal.controller;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
@@ -26,17 +28,29 @@ public class StatusNotifier {
return instance;
}
public synchronized void register(Session session, Listener listener) {
this.listenersBySession.put(session, listener);
public void register(Session session, Listener listener) {
synchronized (this.listenersBySession) {
this.listenersBySession.put(session, listener);
}
}
public synchronized void deregister(Session session) {
this.listenersBySession.remove(session);
public void deregister(Session session) {
synchronized (this.listenersBySession) {
this.listenersBySession.remove(session);
}
}
public synchronized void onStatusChange(long now) {
for (Listener listener : this.listenersBySession.values())
public void onStatusChange(long now) {
for (Listener listener : getAllListeners())
listener.notify(now);
}
private Collection<Listener> getAllListeners() {
// Make a copy of listeners to both avoid concurrent modification
// and reduce synchronization time
synchronized (this.listenersBySession) {
return new ArrayList<>(this.listenersBySession.values());
}
}
}

View File

@@ -1,11 +1,15 @@
package org.qortal.data.account;
import java.math.BigDecimal;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlTransient;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.qortal.utils.Base58;
import io.swagger.v3.oas.annotations.media.Schema;
// All properties to be converted to JSON via JAXB
@@ -71,4 +75,13 @@ public class RewardShareData {
return this.minter;
}
// For debugging
public String toString() {
if (this.minter.equals(this.recipient))
return String.format("Minter/recipient: %s, reward-share public key: %s", this.minter, Base58.encode(this.rewardSharePublicKey));
else
return String.format("Minter: %s, recipient: %s (%s %%), reward-share public key: %s", this.minter, this.recipient, BigDecimal.valueOf(this.sharePercent, 2), Base58.encode(this.rewardSharePublicKey));
}
}

View File

@@ -1,6 +1,8 @@
package org.qortal.network;
import java.util.Arrays;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -13,7 +15,9 @@ import org.qortal.network.message.ChallengeMessage;
import org.qortal.network.message.HelloMessage;
import org.qortal.network.message.Message;
import org.qortal.network.message.Message.MessageType;
import org.qortal.settings.Settings;
import org.qortal.network.message.ResponseMessage;
import org.qortal.utils.DaemonThreadFactory;
import org.qortal.utils.NTP;
import com.google.common.primitives.Bytes;
@@ -27,6 +31,7 @@ public enum Handshake {
@Override
public void action(Peer peer) {
/* Never called */
}
},
HELLO(MessageType.HELLO) {
@@ -164,6 +169,11 @@ public enum Handshake {
peer.setPeersNodeId(Crypto.toNodeAddress(peersPublicKey));
// For inbound peers, we need to go into interim holding state while we compute RESPONSE
if (!peer.isOutbound())
return RESPONDING;
// Handshake completed!
return COMPLETED;
}
@@ -178,28 +188,50 @@ public enum Handshake {
final byte[] data = Crypto.digest(Bytes.concat(sharedSecret, peersChallenge));
// We do this in a new thread as it can take a while...
Thread responseThread = new Thread(() -> {
responseExecutor.execute(() -> {
// Are we still connected?
if (peer.isStopping())
// No point computing for dead peer
return;
Integer nonce = MemoryPoW.compute2(data, POW_BUFFER_SIZE, POW_DIFFICULTY);
Message responseMessage = new ResponseMessage(nonce, data);
if (!peer.sendMessage(responseMessage))
peer.disconnect("failed to send RESPONSE");
});
responseThread.setDaemon(true);
responseThread.start();
// For inbound peers, we should actually be in RESPONDING state.
// So we need to do the extra work to move to COMPLETED state.
if (!peer.isOutbound()) {
peer.setHandshakeStatus(COMPLETED);
Network.getInstance().onHandshakeCompleted(peer);
}
});
}
},
COMPLETED(null) {
// Interim holding state while we compute RESPONSE to send to inbound peer
RESPONDING(null) {
@Override
public Handshake onMessage(Peer peer, Message message) {
// Handshake completed
// Should never be called
return null;
}
@Override
public void action(Peer peer) {
// Note: this is only called when we've made outbound connection
// Should never be called
}
},
COMPLETED(null) {
@Override
public Handshake onMessage(Peer peer, Message message) {
// Should never be called
return null;
}
@Override
public void action(Peer peer) {
// Note: this is only called if we've made outbound connection
}
};
@@ -212,6 +244,7 @@ public enum Handshake {
private static final int POW_BUFFER_SIZE = 8 * 1024 * 1024; // bytes
private static final int POW_DIFFICULTY = 8; // leading zero bits
private static final ExecutorService responseExecutor = Executors.newFixedThreadPool(Settings.getInstance().getNetworkPoWComputePoolSize(), new DaemonThreadFactory("Network-PoW"));
private static final byte[] ZERO_CHALLENGE = new byte[ChallengeMessage.CHALLENGE_LENGTH];

View File

@@ -55,6 +55,7 @@ import org.qortal.utils.ExecuteProduceConsume;
// import org.qortal.utils.ExecutorDumper;
import org.qortal.utils.ExecuteProduceConsume.StatsSnapshot;
import org.qortal.utils.NTP;
import org.qortal.utils.NamedThreadFactory;
// For managing peers
public class Network {
@@ -151,7 +152,8 @@ public class Network {
ExecutorService networkExecutor = new ThreadPoolExecutor(1,
Settings.getInstance().getMaxNetworkThreadPoolSize(),
NETWORK_EPC_KEEPALIVE, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
new SynchronousQueue<Runnable>(),
new NamedThreadFactory("Network-EPC"));
networkEPC = new NetworkProcessor(networkExecutor);
}
@@ -355,7 +357,7 @@ public class Network {
private Task maybeProducePeerPingTask(Long now) {
// Ask connected peers whether they need a ping
for (Peer peer : getConnectedPeers()) {
for (Peer peer : getHandshakedPeers()) {
Task peerTask = peer.getPingTask(now);
if (peerTask != null)
return peerTask;
@@ -635,7 +637,7 @@ public class Network {
/** Called when Peer's thread has setup and is ready to process messages */
public void onPeerReady(Peer peer) {
this.onMessage(peer, null);
onHandshakingMessage(peer, null, Handshake.STARTED);
}
public void onDisconnect(Peer peer) {
@@ -777,7 +779,7 @@ public class Network {
opportunisticMergePeers(peer.toString(), peerV2Addresses);
}
private void onHandshakeCompleted(Peer peer) {
/*pacakge*/ void onHandshakeCompleted(Peer peer) {
LOGGER.debug(String.format("Handshake completed with peer %s", peer));
// Are we already connected to this peer?

View File

@@ -169,8 +169,8 @@ public interface AccountRepository {
public void save(MintingAccountData mintingAccountData) throws DataException;
/** Delete minting account info, used by BlockMinter, from repository using passed private key. */
public int delete(byte[] mintingAccountPrivateKey) throws DataException;
/** Delete minting account info, used by BlockMinter, from repository using passed public or private key. */
public int delete(byte[] mintingAccountKey) throws DataException;
// Managing QORT from legacy QORA

View File

@@ -2,6 +2,7 @@ package org.qortal.repository;
import java.util.List;
import org.qortal.api.model.BlockInfo;
import org.qortal.api.model.BlockSignerSummary;
import org.qortal.data.block.BlockData;
import org.qortal.data.block.BlockSummaryData;
@@ -128,6 +129,11 @@ public interface BlockRepository {
*/
public List<BlockSummaryData> getBlockSummaries(int firstBlockHeight, int lastBlockHeight) throws DataException;
/**
* Returns block infos for the passed height range, for API use.
*/
public List<BlockInfo> getBlockInfos(Integer startHeight, Integer endHeight, Integer count) throws DataException;
/**
* Trim online accounts signatures from blocks older than passed timestamp.
*

View File

@@ -774,9 +774,9 @@ public class HSQLDBAccountRepository implements AccountRepository {
}
@Override
public int delete(byte[] minterPrivateKey) throws DataException {
public int delete(byte[] minterKey) throws DataException {
try {
return this.repository.delete("MintingAccounts", "minter_private_key = ?", minterPrivateKey);
return this.repository.delete("MintingAccounts", "minter_private_key = ? OR minter_public_key = ?", minterKey, minterKey);
} catch (SQLException e) {
throw new DataException("Unable to delete minting account from repository", e);
}

View File

@@ -6,6 +6,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.qortal.api.model.BlockInfo;
import org.qortal.api.model.BlockSignerSummary;
import org.qortal.data.block.BlockData;
import org.qortal.data.block.BlockSummaryData;
@@ -360,6 +361,90 @@ public class HSQLDBBlockRepository implements BlockRepository {
}
}
@Override
public List<BlockInfo> getBlockInfos(Integer startHeight, Integer endHeight, Integer count) throws DataException {
StringBuilder sql = new StringBuilder(512);
sql.append("SELECT signature, height, minted_when, transaction_count, RewardShares.minter ");
/*
* start end count result
* 10 40 null blocks 10 to 39 (excludes end block, ignore count)
*
* null null null blocks 1 to 50 (assume count=50, maybe start=1)
* 30 null null blocks 30 to 79 (assume count=50)
* 30 null 10 blocks 30 to 39
*
* null null 50 last 50 blocks? so if max(blocks.height) is 200, then blocks 151 to 200
* null 200 null blocks 150 to 199 (excludes end block, assume count=50)
* null 200 10 blocks 190 to 199 (excludes end block)
*/
if (startHeight != null && endHeight != null) {
sql.append("FROM Blocks ");
sql.append("JOIN RewardShares ON RewardShares.reward_share_public_key = Blocks.minter ");
sql.append("WHERE height BETWEEN ");
sql.append(startHeight);
sql.append(" AND ");
sql.append(endHeight - 1);
} else if (endHeight != null || (startHeight == null && count != null)) {
// we are going to return blocks from the end of the chain
if (count == null)
count = 50;
if (endHeight == null) {
sql.append("FROM (SELECT height FROM Blocks ORDER BY height DESC LIMIT 1) AS MaxHeights (max_height) ");
sql.append("JOIN Blocks ON height BETWEEN (max_height - ");
sql.append(count);
sql.append(" + 1) AND max_height ");
sql.append("JOIN RewardShares ON RewardShares.reward_share_public_key = Blocks.minter");
} else {
sql.append("FROM Blocks ");
sql.append("JOIN RewardShares ON RewardShares.reward_share_public_key = Blocks.minter ");
sql.append("WHERE height BETWEEN ");
sql.append(endHeight - count);
sql.append(" AND ");
sql.append(endHeight - 1);
}
} else {
// we are going to return blocks from the start of the chain
if (startHeight == null)
startHeight = 1;
if (count == null)
count = 50;
sql.append("FROM Blocks ");
sql.append("JOIN RewardShares ON RewardShares.reward_share_public_key = Blocks.minter ");
sql.append("WHERE height BETWEEN ");
sql.append(startHeight);
sql.append(" AND ");
sql.append(startHeight + count - 1);
}
List<BlockInfo> blockInfos = new ArrayList<>();
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString())) {
if (resultSet == null)
return blockInfos;
do {
byte[] signature = resultSet.getBytes(1);
int height = resultSet.getInt(2);
long timestamp = resultSet.getLong(3);
int transactionCount = resultSet.getInt(4);
String minterAddress = resultSet.getString(5);
BlockInfo blockInfo = new BlockInfo(signature, height, timestamp, transactionCount, minterAddress);
blockInfos.add(blockInfo);
} while (resultSet.next());
return blockInfos;
} catch (SQLException e) {
throw new DataException("Unable to fetch height-ranged block infos from repository", e);
}
}
@Override
public int trimOldOnlineAccountsSignatures(long timestamp) throws DataException {
String sql = "UPDATE Blocks set online_accounts_signatures = NULL WHERE minted_when < ? AND online_accounts_signatures IS NOT NULL";

View File

@@ -47,7 +47,7 @@ public class Settings {
// UI servers
private int uiPort = 12388;
private String[] uiLocalServers = new String[] {
"localhost", "172.24.1.1", "qor.tal"
"localhost", "127.0.0.1", "172.24.1.1", "qor.tal"
};
private String[] uiRemoteServers = new String[] {
"node1.qortal.org", "node2.qortal.org", "node3.qortal.org", "node4.qortal.org", "node5.qortal.org",
@@ -92,6 +92,8 @@ public class Settings {
private int maxPeers = 32;
/** Maximum number of threads for network engine. */
private int maxNetworkThreadPoolSize = 20;
/** Maximum number of threads for network proof-of-work compute, used during handshaking. */
private int networkPoWComputePoolSize = 2;
// Which blockchains this node is running
private String blockchainConfig = null; // use default from resources
@@ -106,7 +108,9 @@ public class Settings {
// Auto-update sources
private String[] autoUpdateRepos = new String[] {
"https://github.com/QORT/qortal/raw/%s/qortal.update",
"https://raw.githubusercontent.com@151.101.16.133/QORT/qortal/%s/qortal.update"
"https://raw.githubusercontent.com@151.101.16.133/QORT/qortal/%s/qortal.update",
"https://github.com/Qortal/qortal/raw/%s/qortal.update",
"https://raw.githubusercontent.com@151.101.16.133/Qortal/qortal/%s/qortal.update"
};
/** Array of NTP server hostnames. */
@@ -353,6 +357,10 @@ public class Settings {
return this.maxNetworkThreadPoolSize;
}
public int getNetworkPoWComputePoolSize() {
return this.networkPoWComputePoolSize;
}
public String getBlockchainConfig() {
return this.blockchainConfig;
}

View File

@@ -136,7 +136,7 @@ public class RewardShareTransaction extends Transaction {
// Deleting a non-existent reward-share makes no sense
if (isCancellingSharePercent)
return ValidationResult.INVALID_REWARD_SHARE_PERCENT;
return ValidationResult.REWARD_SHARE_UNKNOWN;
// Check the minting account hasn't reach maximum number of reward-shares
int rewardShareCount = this.repository.getAccountRepository().countRewardShares(creator.getPublicKey());

View File

@@ -225,6 +225,7 @@ public abstract class Transaction {
AT_IS_FINISHED(71),
NO_FLAG_PERMISSION(72),
NOT_MINTING_ACCOUNT(73),
REWARD_SHARE_UNKNOWN(76),
INVALID_REWARD_SHARE_PERCENT(77),
PUBLIC_KEY_UNKNOWN(78),
INVALID_PUBLIC_KEY(79),

View File

@@ -0,0 +1,31 @@
package org.qortal.utils;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
public class DaemonThreadFactory implements ThreadFactory {
private final String name;
private final AtomicInteger threadNumber = new AtomicInteger(1);
public DaemonThreadFactory(String name) {
this.name = name;
}
public DaemonThreadFactory() {
this(null);
}
@Override
public Thread newThread(Runnable runnable) {
Thread thread = Executors.defaultThreadFactory().newThread(runnable);
thread.setDaemon(true);
if (this.name != null)
thread.setName(this.name + "-" + this.threadNumber.getAndIncrement());
return thread;
}
}

View File

@@ -0,0 +1,24 @@
package org.qortal.utils;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
public class NamedThreadFactory implements ThreadFactory {
private final String name;
private final AtomicInteger threadNumber = new AtomicInteger(1);
public NamedThreadFactory(String name) {
this.name = name;
}
@Override
public Thread newThread(Runnable runnable) {
Thread thread = Executors.defaultThreadFactory().newThread(runnable);
thread.setName(this.name + "-" + this.threadNumber.getAndIncrement());
return thread;
}
}

13
tools/peer-heights Executable file
View File

@@ -0,0 +1,13 @@
#!/usr/bin/env bash
# Requires: 'qort' script in PATH, and 'jq' utility installed
set -e
# Any extra args passed to us are also passed to 'qort', just prior to '-p peers'
qort $@ -p peers | \
jq -r 'def lpad($len):
tostring | ($len - length) as $l | (" " * $l)[:$l] + .;
.[] |
select(has("lastHeight")) |
"\(.address | lpad(22)) (\(.version)), height \(.lastHeight), sig: \(.lastBlockSignature[0:8]), ts \(.lastBlockTimestamp / 1e3 | strftime("%Y-%m-%d %H:%M:%S"))"'

87
tools/qort Executable file
View File

@@ -0,0 +1,87 @@
#!/usr/bin/env bash
# default output post-processor
postproc=cat
# Qortal defaults
port=12391
example_host=node10.qortal.org
# called-as name
name="${0##*/}"
while [ -n "$*" ]; do
case $1 in
-p)
shift
postproc="json_pp -f json -t json -json_opt utf8,pretty"
;;
[Dd][Ee][Ll][Ee][Tt][Ee])
shift
method="-X DELETE"
;;
-l)
shift
src="--interface localhost"
;;
-t)
shift
testnet=true
;;
-j)
shift
content_type="Content-Type: application/json"
;;
*)
break
;;
esac
done
if [ "${name}" = "qort" ]; then
port=${testnet:+62391}
port=${port:-12391}
example_host=node10.qortal.org
fi
if [ -z "$*" ]; then
echo "usage: $name [-l] [-p] [-t] [DELETE] <url> [<post-data>]"
echo "-l: use localhost as source address"
echo "-p: pretty-print JSON output"
echo "-t: use testnet port"
echo "example (using localhost:${port}): $name -p blocks/last"
echo "example: $name -p http://${example_host}:${port}/blocks/last"
echo "example: BASE_URL=http://${example_host}:${port} $name -p blocks/last"
echo "example: BASE_URL=${example_host} $name -p blocks/last"
echo "example: $name -l DELETE peers/known"
exit
fi
url=$1
shift
if [ "${url:0:4}" != "http" ]; then
base_url=${BASE_URL-localhost:${port}}
if [ "${base_url:0:4}" != "http" ]; then
base_url="http://${base_url}"
fi
if [ -n "${base_url/#*:[0-9[0-9]*}" ]; then
base_url="${base_url%%/}:${port}"
fi
url="${base_url%%/}/${url#/}"
fi
if [ "$#" != 0 ]; then
data="--data"
fi
curl --silent --insecure --connect-timeout 5 ${content_type:+--header} "${content_type}" ${method} ${src} --url ${url} ${data} "$@" | ${postproc}
echo

353
tools/tx.pl Executable file
View File

@@ -0,0 +1,353 @@
#!/usr/bin/env perl
use JSON;
use warnings;
use strict;
use Getopt::Std;
use File::Basename;
our %opt;
getopts('dpst', \%opt);
my $proc = basename($0);
if (@ARGV < 1) {
print STDERR "usage: $proc [-d] [-p] [-s] [-t] <tx-type> <privkey> <values> [<key-value pairs>]\n";
print STDERR "-d: debug, -p: process (broadcast) transaction, -s: sign, -t: testnet\n";
print STDERR "example: $proc PAYMENT P22kW91AJfDNBj32nVii292hhfo5AgvUYPz5W12ExsjE QxxQZiK7LZBjmpGjRz1FAZSx9MJDCoaHqz 0.1\n";
print STDERR "example: $proc JOIN_GROUP X92h3hf9k20kBj32nVnoh3XT14o5AgvUYPz5W12ExsjE 3\n";
print STDERR "example: BASE_URL=node10.qortal.org $proc JOIN_GROUP CB2DW91AJfd47432nVnoh3XT14o5AgvUYPz5W12ExsjE 3\n";
print STDERR "example: $proc -p sign C4ifh827ffDNBj32nVnoh3XT14o5AgvUYPz5W12ExsjE 111jivxUwerRw...Fjtu\n";
print STDERR "for help: $proc all\n";
print STDERR "for help: $proc REGISTER_NAME\n";
exit 2;
}
our $BASE_URL = $ENV{BASE_URL} || $opt{t} ? 'http://localhost:62391' : 'http://localhost:12391';
our $DEFAULT_FEE = 0.001;
our %TRANSACTION_TYPES = (
payment => {
url => 'payments/pay',
required => [qw(recipient amount)],
key_name => 'senderPublicKey',
},
# groups
set_group => {
url => 'groups/setdefault',
required => [qw(defaultGroupId)],
key_name => 'creatorPublicKey',
},
create_group => {
url => 'groups/create',
required => [qw(groupName description isOpen approvalThreshold)],
key_name => 'creatorPublicKey',
},
update_group => {
url => 'groups/update',
required => [qw(groupId newOwner newDescription newIsOpen newApprovalThreshold)],
key_name => 'ownerPublicKey',
},
join_group => {
url => 'groups/join',
required => [qw(groupId)],
key_name => 'joinerPublicKey',
},
leave_group => {
url => 'groups/leave',
required => [qw(groupId)],
key_name => 'leaverPublicKey',
},
group_invite => {
url => 'groups/invite',
required => [qw(groupId invitee)],
key_name => 'adminPublicKey',
},
group_kick => {
url => 'groups/kick',
required => [qw(groupId member reason)],
key_name => 'adminPublicKey',
},
add_group_admin => {
url => 'groups/addadmin',
required => [qw(groupId member)],
key_name => 'ownerPublicKey',
},
group_approval => {
url => 'groups/approval',
required => [qw(pendingSignature approval)],
key_name => 'adminPublicKey',
},
# assets
issue_asset => {
url => 'assets/issue',
required => [qw(assetName description quantity isDivisible)],
key_name => 'issuerPublicKey',
},
update_asset => {
url => 'assets/update',
required => [qw(assetId newOwner)],
key_name => 'ownerPublicKey',
},
transfer_asset => {
url => 'assets/transfer',
required => [qw(recipient amount assetId)],
key_name => 'senderPublicKey',
},
create_order => {
url => 'assets/order',
required => [qw(haveAssetId wantAssetId amount price)],
key_name => 'creatorPublicKey',
},
# names
register_name => {
url => 'names/register',
required => [qw(name data)],
key_name => 'registrantPublicKey',
},
update_name => {
url => 'names/update',
required => [qw(newName newData)],
key_name => 'ownerPublicKey',
},
# reward-shares
reward_share => {
url => 'addresses/rewardshare',
required => [qw(recipient rewardSharePublicKey sharePercent)],
key_name => 'minterPublicKey',
},
# arbitrary
arbitrary => {
url => 'arbitrary',
required => [qw(service dataType data)],
key_name => 'senderPublicKey',
},
# chat
chat => {
url => 'chat',
required => [qw(data)],
optional => [qw(recipient isText isEncrypted)],
key_name => 'senderPublicKey',
defaults => { isText => 'true' },
pow_url => 'chat/compute',
},
# misc
publicize => {
url => 'addresses/publicize',
required => [],
key_name => 'senderPublicKey',
pow_url => 'addresses/publicize/compute',
},
# Cross-chain trading
build_trade => {
url => 'crosschain/build',
required => [qw(initialQortAmount finalQortAmount fundingQortAmount secretHash bitcoinAmount)],
optional => [qw(tradeTimeout)],
key_name => 'creatorPublicKey',
defaults => { tradeTimeout => 10800 },
},
trade_recipient => {
url => 'crosschain/tradeoffer/recipient',
required => [qw(atAddress recipient)],
key_name => 'creatorPublicKey',
remove => [qw(timestamp reference fee)],
},
trade_secret => {
url => 'crosschain/tradeoffer/secret',
required => [qw(atAddress secret)],
key_name => 'recipientPublicKey',
remove => [qw(timestamp reference fee)],
},
# These are fake transaction types to provide utility functions:
sign => {
url => 'transactions/sign',
required => [qw{transactionBytes}],
},
);
my $tx_type = lc(shift(@ARGV));
if ($tx_type eq 'all') {
printf STDERR "Transaction types: %s\n", join(', ', sort { $a cmp $b } keys %TRANSACTION_TYPES);
exit 2;
}
my $tx_info = $TRANSACTION_TYPES{$tx_type};
if (!$tx_info) {
printf STDERR "Transaction type '%s' unknown\n", uc($tx_type);
exit 1;
}
my @required = @{$tx_info->{required}};
if (@ARGV < @required + 1) {
printf STDERR "usage: %s %s <privkey> %s", $proc, uc($tx_type), join(' ', map { "<$_>"} @required);
printf STDERR " %s", join(' ', map { "[$_ <$_>]" } @{$tx_info->{optional}}) if exists $tx_info->{optional};
print "\n";
exit 2;
}
my $priv_key = shift @ARGV;
my $account = account($priv_key);
my $raw;
if ($tx_type ne 'sign') {
my %extras;
foreach my $required_arg (@required) {
$extras{$required_arg} = shift @ARGV;
}
# For CHAT we use a random reference
if ($tx_type eq 'chat') {
$extras{reference} = api('utils/random?length=64');
}
%extras = (%extras, %{$tx_info->{defaults}}) if exists $tx_info->{defaults};
%extras = (%extras, @ARGV);
$raw = build_raw($tx_type, $account, %extras);
printf "Raw: %s\n", $raw if $opt{d} || (!$opt{s} && !$opt{p});
# Some transaction types require proof-of-work, e.g. CHAT
if (exists $tx_info->{pow_url}) {
$raw = api($tx_info->{pow_url}, $raw);
printf "Raw with PoW: %s\n", $raw if $opt{d};
}
} else {
$raw = shift @ARGV;
$opt{s}++;
}
if ($opt{s}) {
my $signed = sign($account->{private}, $raw);
printf "Signed: %s\n", $signed if $opt{d} || $tx_type eq 'sign';
if ($opt{p}) {
my $processed = process($signed);
printf "Processed: %s\n", $processed if $opt{d};
}
my $hex = api('utils/frombase58', $signed);
# sig is last 64 bytes / 128 chars
my $sighex = substr($hex, -128);
my $sig58 = api('utils/tobase58/{hex}', '', '{hex}', $sighex);
printf "Signature: %s\n", $sig58;
}
sub account {
my ($creator) = @_;
my $account = { private => $creator };
$account->{public} = api('utils/publickey', $creator);
$account->{address} = api('addresses/convert/{publickey}', '', '{publickey}', $account->{public});
return $account;
}
sub build_raw {
my ($type, $account, %extras) = @_;
my $tx_info = $TRANSACTION_TYPES{$type};
die("unknown tx type: $type\n") unless defined $tx_info;
my $ref = exists $extras{reference} ? $extras{reference} : lastref($account->{address});
my %json = (
timestamp => time * 1000,
reference => $ref,
fee => $DEFAULT_FEE,
);
$json{$tx_info->{key_name}} = $account->{public} if exists $tx_info->{key_name};
foreach my $required (@{$tx_info->{required}}) {
die("missing tx field: $required\n") unless exists $extras{$required};
}
while (my ($key, $value) = each %extras) {
$json{$key} = $value;
}
if (exists $tx_info->{remove}) {
foreach my $key (@{$tx_info->{remove}}) {
delete $json{$key};
}
}
my $json = "{\n";
while (my ($key, $value) = each %json) {
if (ref($value) eq 'ARRAY') {
$json .= "\t\"$key\": [],\n";
} else {
$json .= "\t\"$key\": \"$value\",\n";
}
}
# remove final comma
substr($json, -2, 1) = '';
$json .= "}\n";
printf "%s:\n%s\n", $type, $json if $opt{d};
my $raw = api($tx_info->{url}, $json);
return $raw;
}
sub sign {
my ($private, $raw) = @_;
my $json = <<" __JSON__";
{
"privateKey": "$private",
"transactionBytes": "$raw"
}
__JSON__
return api('transactions/sign', $json);
}
sub process {
my ($signed) = @_;
return api('transactions/process', $signed);
}
sub lastref {
my ($address) = @_;
return api('addresses/lastreference/{address}', '', '{address}', $address)
}
sub api {
my ($endpoint, $postdata, @args) = @_;
my $url = $endpoint;
my $method = 'GET';
for (my $i = 0; $i < @args; $i += 2) {
my $placemarker = $args[$i];
my $value = $args[$i + 1];
$url =~ s/$placemarker/$value/g;
}
my $curl = "curl --silent --output - --url '$BASE_URL/$url'";
if (defined $postdata && $postdata ne '') {
$postdata =~ tr|\n| |s;
$curl .= " --header 'Content-Type: application/json' --data-binary '$postdata'";
$method = 'POST';
}
my $response = `$curl 2>/dev/null`;
chomp $response;
if ($response eq '' || substr($response, 0, 6) eq '<html>' || $response =~ m/(^\{|,)"error":(\d+)[,}]/) {
die("API call '$method $BASE_URL/$endpoint' failed:\n$response\n");
}
return $response;
}