Compare commits

...

40 Commits

Author SHA1 Message Date
CalDescent
1ea1e00344 Bump version to 3.3.0 2022-05-09 18:47:19 +01:00
CalDescent
adecb21ada Added mainnet xpub addresses for DGB and RVN tests, as testnet support isn't fully implemented yet. 2022-05-07 17:40:36 +01:00
CalDescent
dac484136f Fixed bug in name rebuilding. 2022-05-07 16:46:10 +01:00
CalDescent
6c201db3dd Merge branch 'EPC-fixes' 2022-05-02 08:28:14 +01:00
CalDescent
da47df0a25 Fixed merge issue. 2022-05-01 16:41:56 +01:00
CalDescent
eea215dacf Merge branch 'ravencoin' into new-coins
# Conflicts:
#	pom.xml
#	src/main/java/org/qortal/controller/tradebot/TradeBot.java
#	src/main/java/org/qortal/crosschain/SupportedBlockchain.java
#	src/main/java/org/qortal/settings/Settings.java
2022-05-01 16:28:22 +01:00
CalDescent
0949271dda Merge branch 'digibyte' into new-coins 2022-05-01 16:25:40 +01:00
CalDescent
6bb9227159 Removed RavencoinACCTv1
Also removed CrossChainRavencoinACCTv1Resource - same as Digibyte.
2022-05-01 16:17:00 +01:00
CalDescent
a95a37277c Removed DigibyteACCTv1 and v2
Also removed CrossChainDigibyteACCTv1Resource, since this is unused, and it seems excessive to maintain support of this for every coin (and potentially every ACCT version).
2022-05-01 16:12:12 +01:00
CalDescent
48b9aa5c18 Allow images to be displayed in QDN websites via data: and blob: 2022-05-01 14:37:57 +01:00
CalDescent
fbe4f3fad8 Fixed incorrect minOutboundPeers conditional 2022-05-01 14:01:58 +01:00
CalDescent
599877195b Merge branch 'master' into EPC-fixes 2022-04-30 15:32:44 +01:00
CalDescent
0695039ee3 Fixed long term bug causing last line to be missed out. 2022-04-30 12:08:10 +01:00
CalDescent
a4bcd4451c Added "tail" parameter to GET /admin/logs to allow returning the last X (limit) lines.
This should make it easy to display core logs in the UI.
2022-04-30 12:07:47 +01:00
CalDescent
e5b4b61832 Fixed bugs causing "Hash ... does not match file digest ..." errors 2022-04-30 11:26:05 +01:00
CalDescent
dd55dc277b Updated AdvancedInstaller project for v3.2.5 2022-04-27 08:50:40 +01:00
QuickMythril
0f52ccb433 add Ravencoin ACCTs 2022-04-26 13:51:19 -04:00
QuickMythril
8aed84e6af add Digibyte ACCTs 2022-04-26 11:40:42 -04:00
CalDescent
f9972f50e0 Updated altcoinj 2022-04-24 15:08:43 +01:00
QuickMythril
05d9a7e820 Switched to Qortal fork of altcoinj, using RavencoinMainNetParams 2022-04-23 08:28:12 -04:00
CalDescent
b1f184c493 Use DigibyteMainNetParams 2022-04-22 16:31:44 +01:00
CalDescent
d66dd51bf6 Switched to Qortal fork of altcoinj, with Digibyte support 2022-04-22 16:31:32 +01:00
QuickMythril
0baed55a44 add DGB wallet 2022-04-21 11:40:17 -04:00
QuickMythril
390b359761 add RVN wallet 2022-04-21 11:38:49 -04:00
CalDescent
70eaaa9e3b Merge remote-tracking branch 'catbref/EPC-fixes' into EPC-fixes 2022-04-19 20:50:32 +01:00
catbref
3e622f7185 EPC-fixes: catch CancelledKeyExceptions thrown in short window between nextSelectionKey.isValid() and nextSelectionKey.isXXXable() calls 2022-04-18 14:33:05 +01:00
CalDescent
3f12be50ac Merge remote-tracking branch 'catbref/EPC-fixes' into EPC-fixes 2022-04-18 09:20:55 +01:00
catbref
68412b49a1 EPC-fixes: use bindAddress from Settings for outgoing peer connections, not just listen socket 2022-04-17 19:38:50 +01:00
catbref
c9b2620461 EPC-fixes: fix constructing GET_ONLINE_ACCOUNTS_V2 message for case where onlineAccount args is empty list 2022-04-17 19:37:28 +01:00
catbref
df3f16ccf1 EPC-fixes: Improve Network shutdown by exiting fast during broadcast and skipping callbacks during peer disconnect. 2022-04-17 14:50:15 +01:00
catbref
22aa5c41b5 WIP: EPC-fixes
BlockMessage was broken because the repository 'connection' associated with the message's Block object was closed between message queuing and message sending.

The fix was to serialize Message subclasses on construction, thus freeing reliance on objects passed into constructor.
The serialized byte[] is held by the message between queuing and sending.
This forces messages into one of two 'modes': outgoing or incoming.
Outgoing messages contain serialized byte[] whereas incoming messages unpack a ByteBuffer into Message subclass fields.
As a result, all network message types have been refactored in this way.
More details in Message's class comment.

A knock-on effect is that incoming messages cannot then be sent out - a new message needs to be constructed.
Some changes needed to Arbitrary controller package classes in this respect.

Bonus: Network no longer needs broadcast threads because 'broadcasting' is now simply the act of queuing a message for many peers.
2022-04-17 14:50:15 +01:00
catbref
8e09567221 EPC-fixed: avoiding some CancelledKeyExceptions 2022-04-17 14:50:15 +01:00
catbref
3505788d42 Another chunk of improvements to networking / EPC.
Instead of synchronizing/blocking in Peer.sendMessage(),
we queue messages in a concurrent blocking TransferQueue, with timeout.

In EPC, ChannelWriteTasks consume from TransferQueue, unblocking callers to Peer.sendMessage().

If a new message is to be sent, or socket output buffer is full,
then OP_WRITE is used to wait for socket to become writable again.

Only one ChannelWriteTask per peer can be active/pending at a time.
Each ChannelWriteTask tries to send as much as it can in one go.

Other minor tidy-ups.
2022-04-17 14:50:15 +01:00
catbref
91e0c9b940 More improvements to networking:
As per work done by szisti in PR#45:
Extracted network 'Tasks' to their own classes.
Network.NetworkProcessor reduced to only producing Tasks.

Improved usage of SocketChannel interest-ops.
Eventually this might lead to reducing task-producing synchronization lock into more granular locks.
Work still needed to convert sending messages to a queue and to make use of OP_WRITE instead of sleeping to wait for socket buffer to empty.

Disabled PeerConnectTask producer from checking against connected peers via DNS as it's too slow.

Swapped Peer's replyQueues from SynchronizedMap(wrapped HashMap) to ConcurrentHashMap.

Other minor changes within networking.
2022-04-17 14:50:15 +01:00
catbref
00996b047f Networking work-in-progress:
As per work done by szisti in PR#45:
Extracted MessageException from inside Message into its own class.
Extracted MessageType from inside Message into its own class.

Converted reflection-based Message.fromByteBuffer method call to non-reflection, functional interface, method-reference.
This should have minor performance improvement but stronger method signature and type enforcement, as well as better IDE integration.

Message.fromByteBuffer method 'contract' tightened up to:
1. throw BufferUnderflowException if there are not enough bytes to deserialize message
2. throw MessageException if bytes contain invalid data
3. should not return null

Message.toData method 'contract' tightened up to:
1. return null if the message has no payload to serialize
2. throw IOException directly - no need to try-catch in each subclass

Several Message-subclass fields now marked 'final' as per IDE suggestion.
Several Message-subclass fromByteBuffer() method signatures have changed 'throws' list.
Several bytes.remaining() != some-value changed to bytes.remaining() < some-value as per new contract.

Some bytes.remaining() checks removed for fixed-length messages because we can rely on ByteBuffer throwing BufferUnderflowException.
Some bytes.remaining() checks retained for variable-length messages, or messages that read a large amount of data, to prevent wasted memory allocations.

Other minor tidying up
2022-04-17 14:50:15 +01:00
catbref
44fc0f367d Networking work-in-progress:
Temporarily increase sleep from 1ms to 100ms when waiting for outgoing socket buffer to empty.

Real fix is to rewrite using an outgoing message queue and OP_WRITE interest op.
2022-04-17 14:50:15 +01:00
catbref
b0e6259073 Networking work-in-progress:
De-register a peer's socket channel OP_READ interest op when producing a ChannelTask for that peer.
This should prevent duplicate ChannelTasks for the same peer.

Re-register OP_READ once node has read from peer's channel.
2022-04-17 14:50:15 +01:00
catbref
6255b2a907 Networking work-in-progress:
When node has reached max connections, Network will ignore pending incoming connections by:
1. not calling accept()
2. de-registering OP_ACCEPT 'interest op' on the listen socket's channel

When a peer disconnects, Network might re-register OP_ACCEPT interest op on listen socket.
2022-04-17 14:50:15 +01:00
catbref
a5fb0be274 Fix Network.disconnectPeer(PeerAddress) to prevent removeIf() on UnmodifiableList throwing UnsupportedOperationException 2022-04-17 14:50:15 +01:00
catbref
e835f6d998 ExecuteProduceConsume:
Slight reworking of EPC to simplify when producer can block
and generally make some of the conditional code more readable.

Improved logging with task class names and logging level editable during runtime!
Use /peer/enginestats?newLoggingLevel=DEBUG (or TRACE or back to INFO) to change.
2022-04-17 14:50:15 +01:00
84 changed files with 7947 additions and 1762 deletions

View File

@@ -17,10 +17,10 @@
<ROW Property="Manufacturer" Value="Qortal"/>
<ROW Property="MsiLogging" MultiBuildValue="DefaultBuild:vp"/>
<ROW Property="NTP_GOOD" Value="false"/>
<ROW Property="ProductCode" Value="1033:{0A40181D-6147-451B-ADA1-782F238DA2EF} 1049:{174FDC95-8017-43BC-A1F9-C96EFE1D4249} 2052:{4DF7B7E2-9CF2-4BE2-8CC4-AC2F2FFD3737} 2057:{0C9E4151-81C6-4726-AE80-E08DA8519DE4} " Type="16"/>
<ROW Property="ProductCode" Value="1033:{8C9CFB9D-BC4C-4142-A5A5-5551BF3B9467} 1049:{4A5BDDD9-ED71-431A-A46F-D19E9DE17216} 2052:{0B9DCE00-BE23-434D-BD6A-1CFA6AB3CA43} 2057:{23D81967-556A-41B8-9981-A739E2820624} " Type="16"/>
<ROW Property="ProductLanguage" Value="2057"/>
<ROW Property="ProductName" Value="Qortal"/>
<ROW Property="ProductVersion" Value="3.2.4" Type="32"/>
<ROW Property="ProductVersion" Value="3.2.5" Type="32"/>
<ROW Property="RECONFIG_NTP" Value="true"/>
<ROW Property="REMOVE_BLOCKCHAIN" Value="YES" Type="4"/>
<ROW Property="REPAIR_BLOCKCHAIN" Value="YES" Type="4"/>
@@ -212,7 +212,7 @@
<ROW Component="ADDITIONAL_LICENSE_INFO_71" ComponentId="{12A3ADBE-BB7A-496C-8869-410681E6232F}" Directory_="jdk.zipfs_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_71" Type="0"/>
<ROW Component="ADDITIONAL_LICENSE_INFO_8" ComponentId="{D53AD95E-CF96-4999-80FC-5812277A7456}" Directory_="java.naming_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_8" Type="0"/>
<ROW Component="ADDITIONAL_LICENSE_INFO_9" ComponentId="{6B7EA9B0-5D17-47A8-B78C-FACE86D15E01}" Directory_="java.net.http_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_9" Type="0"/>
<ROW Component="AI_CustomARPName" ComponentId="{707946B1-D47E-4818-BD7B-60346E94940B}" Directory_="APPDIR" Attributes="260" KeyPath="DisplayName" Options="1"/>
<ROW Component="AI_CustomARPName" ComponentId="{D5A1DC7D-914F-4425-8BA6-A1AE05D0F361}" Directory_="APPDIR" Attributes="260" KeyPath="DisplayName" Options="1"/>
<ROW Component="AI_ExePath" ComponentId="{3644948D-AE0B-41BB-9FAF-A79E70490A08}" Directory_="APPDIR" Attributes="260" KeyPath="AI_ExePath"/>
<ROW Component="APPDIR" ComponentId="{680DFDDE-3FB4-47A5-8FF5-934F576C6F91}" Directory_="APPDIR" Attributes="0"/>
<ROW Component="AccessBridgeCallbacks.h" ComponentId="{288055D1-1062-47A3-AA44-5601B4E38AED}" Directory_="bridge_Dir" Attributes="0" KeyPath="AccessBridgeCallbacks.h" Type="0"/>

View File

@@ -3,11 +3,11 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.qortal</groupId>
<artifactId>qortal</artifactId>
<version>3.2.5</version>
<version>3.3.0</version>
<packaging>jar</packaging>
<properties>
<skipTests>true</skipTests>
<altcoinj.version>bf9fb80</altcoinj.version>
<altcoinj.version>6628cfd</altcoinj.version>
<bitcoinj.version>0.15.10</bitcoinj.version>
<bouncycastle.version>1.64</bouncycastle.version>
<build.timestamp>${maven.build.timestamp}</build.timestamp>
@@ -444,7 +444,7 @@
</dependency>
<!-- For Litecoin, etc. support, requires bitcoinj -->
<dependency>
<groupId>com.github.jjos2372</groupId>
<groupId>com.github.qortal</groupId>
<artifactId>altcoinj</artifactId>
<version>${altcoinj.version}</version>
</dependency>

View File

@@ -0,0 +1,29 @@
package org.qortal.api.model.crosschain;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class DigibyteSendRequest {
@Schema(description = "Digibyte BIP32 extended private key", example = "tprv___________________________________________________________________________________________________________")
public String xprv58;
@Schema(description = "Recipient's Digibyte address ('legacy' P2PKH only)", example = "1DigByteEaterAddressDontSendf59kuE")
public String receivingAddress;
@Schema(description = "Amount of DGB to send", type = "number")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long digibyteAmount;
@Schema(description = "Transaction fee per byte (optional). Default is 0.00000100 DGB (100 sats) per byte", example = "0.00000100", type = "number")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public Long feePerByte;
public DigibyteSendRequest() {
}
}

View File

@@ -0,0 +1,29 @@
package org.qortal.api.model.crosschain;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class RavencoinSendRequest {
@Schema(description = "Ravencoin BIP32 extended private key", example = "tprv___________________________________________________________________________________________________________")
public String xprv58;
@Schema(description = "Recipient's Ravencoin address ('legacy' P2PKH only)", example = "1RvnCoinEaterAddressDontSendf59kuE")
public String receivingAddress;
@Schema(description = "Amount of RVN to send", type = "number")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public long ravencoinAmount;
@Schema(description = "Transaction fee per byte (optional). Default is 0.00000100 RVN (100 sats) per byte", example = "0.00000100", type = "number")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
public Long feePerByte;
public RavencoinSendRequest() {
}
}

View File

@@ -381,6 +381,10 @@ public class AdminResource {
) @QueryParam("limit") Integer limit, @Parameter(
ref = "offset"
) @QueryParam("offset") Integer offset, @Parameter(
name = "tail",
description = "Fetch most recent log lines",
schema = @Schema(type = "boolean")
) @QueryParam("tail") Boolean tail, @Parameter(
ref = "reverse"
) @QueryParam("reverse") Boolean reverse) {
LoggerContext loggerContext = (LoggerContext) LogManager.getContext();
@@ -396,6 +400,13 @@ public class AdminResource {
if (reverse != null && reverse)
logLines = Lists.reverse(logLines);
// Tail mode - return the last X lines (where X = limit)
if (tail != null && tail) {
if (limit != null && limit > 0) {
offset = logLines.size() - limit;
}
}
// offset out of bounds?
if (offset != null && (offset < 0 || offset >= logLines.size()))
return "";
@@ -416,7 +427,7 @@ public class AdminResource {
limit = Math.min(limit, logLines.size());
logLines.subList(limit - 1, logLines.size()).clear();
logLines.subList(limit, logLines.size()).clear();
return String.join("\n", logLines);
} catch (IOException e) {

View File

@@ -0,0 +1,177 @@
package org.qortal.api.resource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import org.bitcoinj.core.Transaction;
import org.qortal.api.ApiError;
import org.qortal.api.ApiErrors;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.Security;
import org.qortal.api.model.crosschain.DigibyteSendRequest;
import org.qortal.crosschain.Digibyte;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.SimpleTransaction;
@Path("/crosschain/dgb")
@Tag(name = "Cross-Chain (Digibyte)")
public class CrossChainDigibyteResource {
@Context
HttpServletRequest request;
@POST
@Path("/walletbalance")
@Operation(
summary = "Returns DGB balance for hierarchical, deterministic BIP32 wallet",
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "BIP32 'm' private/public key in base58",
example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
)
)
),
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", description = "balance (satoshis)"))
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
@SecurityRequirement(name = "apiKey")
public String getDigibyteWalletBalance(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
Security.checkApiCallAllowed(request);
Digibyte digibyte = Digibyte.getInstance();
if (!digibyte.isValidDeterministicKey(key58))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
try {
Long balance = digibyte.getWalletBalanceFromTransactions(key58);
if (balance == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
return balance.toString();
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
}
@POST
@Path("/wallettransactions")
@Operation(
summary = "Returns transactions for hierarchical, deterministic BIP32 wallet",
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "BIP32 'm' private/public key in base58",
example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
)
)
),
responses = {
@ApiResponse(
content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
@SecurityRequirement(name = "apiKey")
public List<SimpleTransaction> getDigibyteWalletTransactions(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
Security.checkApiCallAllowed(request);
Digibyte digibyte = Digibyte.getInstance();
if (!digibyte.isValidDeterministicKey(key58))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
try {
return digibyte.getWalletTransactions(key58);
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
}
@POST
@Path("/send")
@Operation(
summary = "Sends DGB from hierarchical, deterministic BIP32 wallet to specific address",
description = "Currently supports 'legacy' P2PKH Digibyte addresses and Native SegWit (P2WPKH) addresses. Supply BIP32 'm' private key in base58, starting with 'xprv' for mainnet, 'tprv' for testnet",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = DigibyteSendRequest.class
)
)
),
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", description = "transaction hash"))
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.FOREIGN_BLOCKCHAIN_BALANCE_ISSUE, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
@SecurityRequirement(name = "apiKey")
public String sendBitcoin(@HeaderParam(Security.API_KEY_HEADER) String apiKey, DigibyteSendRequest digibyteSendRequest) {
Security.checkApiCallAllowed(request);
if (digibyteSendRequest.digibyteAmount <= 0)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
if (digibyteSendRequest.feePerByte != null && digibyteSendRequest.feePerByte <= 0)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
Digibyte digibyte = Digibyte.getInstance();
if (!digibyte.isValidAddress(digibyteSendRequest.receivingAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
if (!digibyte.isValidDeterministicKey(digibyteSendRequest.xprv58))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
Transaction spendTransaction = digibyte.buildSpend(digibyteSendRequest.xprv58,
digibyteSendRequest.receivingAddress,
digibyteSendRequest.digibyteAmount,
digibyteSendRequest.feePerByte);
if (spendTransaction == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_BALANCE_ISSUE);
try {
digibyte.broadcastTransaction(spendTransaction);
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
return spendTransaction.getTxId().toString();
}
}

View File

@@ -0,0 +1,177 @@
package org.qortal.api.resource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import org.bitcoinj.core.Transaction;
import org.qortal.api.ApiError;
import org.qortal.api.ApiErrors;
import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.Security;
import org.qortal.api.model.crosschain.RavencoinSendRequest;
import org.qortal.crosschain.Ravencoin;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.SimpleTransaction;
@Path("/crosschain/rvn")
@Tag(name = "Cross-Chain (Ravencoin)")
public class CrossChainRavencoinResource {
@Context
HttpServletRequest request;
@POST
@Path("/walletbalance")
@Operation(
summary = "Returns RVN balance for hierarchical, deterministic BIP32 wallet",
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "BIP32 'm' private/public key in base58",
example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
)
)
),
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", description = "balance (satoshis)"))
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
@SecurityRequirement(name = "apiKey")
public String getRavencoinWalletBalance(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
Security.checkApiCallAllowed(request);
Ravencoin ravencoin = Ravencoin.getInstance();
if (!ravencoin.isValidDeterministicKey(key58))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
try {
Long balance = ravencoin.getWalletBalanceFromTransactions(key58);
if (balance == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
return balance.toString();
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
}
@POST
@Path("/wallettransactions")
@Operation(
summary = "Returns transactions for hierarchical, deterministic BIP32 wallet",
description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "BIP32 'm' private/public key in base58",
example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
)
)
),
responses = {
@ApiResponse(
content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
@SecurityRequirement(name = "apiKey")
public List<SimpleTransaction> getRavencoinWalletTransactions(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String key58) {
Security.checkApiCallAllowed(request);
Ravencoin ravencoin = Ravencoin.getInstance();
if (!ravencoin.isValidDeterministicKey(key58))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
try {
return ravencoin.getWalletTransactions(key58);
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
}
@POST
@Path("/send")
@Operation(
summary = "Sends RVN from hierarchical, deterministic BIP32 wallet to specific address",
description = "Currently only supports 'legacy' P2PKH Ravencoin addresses. Supply BIP32 'm' private key in base58, starting with 'xprv' for mainnet, 'tprv' for testnet",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = RavencoinSendRequest.class
)
)
),
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", description = "transaction hash"))
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.FOREIGN_BLOCKCHAIN_BALANCE_ISSUE, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
@SecurityRequirement(name = "apiKey")
public String sendBitcoin(@HeaderParam(Security.API_KEY_HEADER) String apiKey, RavencoinSendRequest ravencoinSendRequest) {
Security.checkApiCallAllowed(request);
if (ravencoinSendRequest.ravencoinAmount <= 0)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
if (ravencoinSendRequest.feePerByte != null && ravencoinSendRequest.feePerByte <= 0)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
Ravencoin ravencoin = Ravencoin.getInstance();
if (!ravencoin.isValidAddress(ravencoinSendRequest.receivingAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
if (!ravencoin.isValidDeterministicKey(ravencoinSendRequest.xprv58))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
Transaction spendTransaction = ravencoin.buildSpend(ravencoinSendRequest.xprv58,
ravencoinSendRequest.receivingAddress,
ravencoinSendRequest.ravencoinAmount,
ravencoinSendRequest.feePerByte);
if (spendTransaction == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_BALANCE_ISSUE);
try {
ravencoin.broadcastTransaction(spendTransaction);
} catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
}
return spendTransaction.getTxId().toString();
}
}

View File

@@ -20,6 +20,11 @@ import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.LoggerConfig;
import org.apache.logging.log4j.core.LoggerContext;
import org.qortal.api.*;
import org.qortal.api.model.ConnectedPeer;
import org.qortal.api.model.PeersSummary;
@@ -127,9 +132,29 @@ public class PeersResource {
}
)
@SecurityRequirement(name = "apiKey")
public ExecuteProduceConsume.StatsSnapshot getEngineStats(@HeaderParam(Security.API_KEY_HEADER) String apiKey) {
public ExecuteProduceConsume.StatsSnapshot getEngineStats(@HeaderParam(Security.API_KEY_HEADER) String apiKey, @QueryParam("newLoggingLevel") Level newLoggingLevel) {
Security.checkApiCallAllowed(request);
if (newLoggingLevel != null) {
final LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
final Configuration config = ctx.getConfiguration();
String epcClassName = "org.qortal.network.Network.NetworkProcessor";
LoggerConfig loggerConfig = config.getLoggerConfig(epcClassName);
LoggerConfig specificConfig = loggerConfig;
// We need a specific configuration for this logger,
// otherwise we would change the level of all other loggers
// having the original configuration as parent as well
if (!loggerConfig.getName().equals(epcClassName)) {
specificConfig = new LoggerConfig(epcClassName, newLoggingLevel, true);
specificConfig.setParent(loggerConfig);
config.addLogger(epcClassName, specificConfig);
}
specificConfig.setLevel(newLoggingLevel);
ctx.updateLoggers();
}
return Network.getInstance().getStatsSnapshot();
}

View File

@@ -93,10 +93,12 @@ public class ArbitraryDataFile {
File outputFile = outputFilePath.toFile();
try (FileOutputStream outputStream = new FileOutputStream(outputFile)) {
outputStream.write(fileContent);
outputStream.close();
this.filePath = outputFilePath;
// Verify hash
if (!this.hash58.equals(this.digest58())) {
LOGGER.error("Hash {} does not match file digest {} for signature: {}", this.hash58, this.digest58(), Base58.encode(signature));
String digest58 = this.digest58();
if (!this.hash58.equals(digest58)) {
LOGGER.error("Hash {} does not match file digest {} for signature: {}", this.hash58, digest58, Base58.encode(signature));
this.delete();
throw new DataException("Data file digest validation failed");
}

View File

@@ -120,7 +120,7 @@ public class ArbitraryDataRenderer {
byte[] data = Files.readAllBytes(Paths.get(filePath)); // TODO: limit file size that can be read into memory
HTMLParser htmlParser = new HTMLParser(resourceId, inPath, prefix, usePrefix, data);
htmlParser.addAdditionalHeaderTags();
response.addHeader("Content-Security-Policy", "default-src 'self' 'unsafe-inline' 'unsafe-eval'; media-src 'self' blob:");
response.addHeader("Content-Security-Policy", "default-src 'self' 'unsafe-inline' 'unsafe-eval'; media-src 'self' blob:; img-src 'self' data: blob:;");
response.setContentType(context.getMimeType(filename));
response.setContentLength(htmlParser.getData().length);
response.getOutputStream().write(htmlParser.getData());

View File

@@ -58,6 +58,7 @@ import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory;
import org.qortal.settings.Settings;
import org.qortal.transaction.Transaction;
import org.qortal.transaction.Transaction.TransactionType;
import org.qortal.transform.TransformationException;
import org.qortal.utils.*;
public class Controller extends Thread {
@@ -1218,7 +1219,7 @@ public class Controller extends Thread {
this.stats.getBlockMessageStats.cacheHits.incrementAndGet();
// We need to duplicate it to prevent multiple threads setting ID on the same message
CachedBlockMessage clonedBlockMessage = cachedBlockMessage.cloneWithNewId(message.getId());
CachedBlockMessage clonedBlockMessage = Message.cloneWithNewId(cachedBlockMessage, message.getId());
if (!peer.sendMessage(clonedBlockMessage))
peer.disconnect("failed to send block");
@@ -1277,7 +1278,6 @@ public class Controller extends Thread {
CachedBlockMessage blockMessage = new CachedBlockMessage(block);
blockMessage.setId(message.getId());
// This call also causes the other needed data to be pulled in from repository
if (!peer.sendMessage(blockMessage)) {
peer.disconnect("failed to send block");
// Don't fall-through to caching because failure to send might be from failure to build message
@@ -1291,7 +1291,9 @@ public class Controller extends Thread {
this.blockMessageCache.put(ByteArray.wrap(blockData.getSignature()), blockMessage);
}
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while send block %s to peer %s", Base58.encode(signature), peer), e);
LOGGER.error(String.format("Repository issue while sending block %s to peer %s", Base58.encode(signature), peer), e);
} catch (TransformationException e) {
LOGGER.error(String.format("Serialization issue while sending block %s to peer %s", Base58.encode(signature), peer), e);
}
}

View File

@@ -33,7 +33,7 @@ import org.qortal.network.message.GetBlockSummariesMessage;
import org.qortal.network.message.GetSignaturesV2Message;
import org.qortal.network.message.Message;
import org.qortal.network.message.SignaturesMessage;
import org.qortal.network.message.Message.MessageType;
import org.qortal.network.message.MessageType;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;

View File

@@ -12,6 +12,7 @@ import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.transaction.Transaction;
import org.qortal.transform.TransformationException;
import org.qortal.utils.Base58;
import org.qortal.utils.NTP;
@@ -289,7 +290,9 @@ public class TransactionImporter extends Thread {
if (!peer.sendMessage(transactionMessage))
peer.disconnect("failed to send transaction");
} catch (DataException e) {
LOGGER.error(String.format("Repository issue while send transaction %s to peer %s", Base58.encode(signature), peer), e);
LOGGER.error(String.format("Repository issue while sending transaction %s to peer %s", Base58.encode(signature), peer), e);
} catch (TransformationException e) {
LOGGER.error(String.format("Serialization issue while sending transaction %s to peer %s", Base58.encode(signature), peer), e);
}
}

View File

@@ -511,18 +511,23 @@ public class ArbitraryDataFileListManager {
// Bump requestHops if it exists
if (requestHops != null) {
arbitraryDataFileListMessage.setRequestHops(++requestHops);
requestHops++;
}
ArbitraryDataFileListMessage forwardArbitraryDataFileListMessage;
// Remove optional parameters if the requesting peer doesn't support it yet
// A message with less statistical data is better than no message at all
if (!requestingPeer.isAtLeastVersion(MIN_PEER_VERSION_FOR_FILE_LIST_STATS)) {
arbitraryDataFileListMessage.removeOptionalStats();
forwardArbitraryDataFileListMessage = new ArbitraryDataFileListMessage(signature, hashes);
} else {
forwardArbitraryDataFileListMessage = new ArbitraryDataFileListMessage(signature, hashes, requestTime, requestHops,
arbitraryDataFileListMessage.getPeerAddress(), arbitraryDataFileListMessage.isRelayPossible());
}
// Forward to requesting peer
LOGGER.debug("Forwarding file list with {} hashes to requesting peer: {}", hashes.size(), requestingPeer);
if (!requestingPeer.sendMessage(arbitraryDataFileListMessage)) {
if (!requestingPeer.sendMessage(forwardArbitraryDataFileListMessage)) {
requestingPeer.disconnect("failed to forward arbitrary data file list");
}
}
@@ -639,16 +644,19 @@ public class ArbitraryDataFileListManager {
}
String ourAddress = Network.getInstance().getOurExternalIpAddressAndPort();
ArbitraryDataFileListMessage arbitraryDataFileListMessage = new ArbitraryDataFileListMessage(signature,
hashes, NTP.getTime(), 0, ourAddress, true);
arbitraryDataFileListMessage.setId(message.getId());
ArbitraryDataFileListMessage arbitraryDataFileListMessage;
// Remove optional parameters if the requesting peer doesn't support it yet
// A message with less statistical data is better than no message at all
if (!peer.isAtLeastVersion(MIN_PEER_VERSION_FOR_FILE_LIST_STATS)) {
arbitraryDataFileListMessage.removeOptionalStats();
arbitraryDataFileListMessage = new ArbitraryDataFileListMessage(signature, hashes);
} else {
arbitraryDataFileListMessage = new ArbitraryDataFileListMessage(signature,
hashes, NTP.getTime(), 0, ourAddress, true);
}
arbitraryDataFileListMessage.setId(message.getId());
if (!peer.sendMessage(arbitraryDataFileListMessage)) {
LOGGER.debug("Couldn't send list of hashes");
peer.disconnect("failed to send list of hashes");
@@ -670,8 +678,7 @@ public class ArbitraryDataFileListManager {
// In relay mode - so ask our other peers if they have it
long requestTime = getArbitraryDataFileListMessage.getRequestTime();
int requestHops = getArbitraryDataFileListMessage.getRequestHops();
getArbitraryDataFileListMessage.setRequestHops(++requestHops);
int requestHops = getArbitraryDataFileListMessage.getRequestHops() + 1;
long totalRequestTime = now - requestTime;
if (totalRequestTime < RELAY_REQUEST_MAX_DURATION) {
@@ -679,11 +686,13 @@ public class ArbitraryDataFileListManager {
if (requestHops < RELAY_REQUEST_MAX_HOPS) {
// Relay request hasn't reached the maximum number of hops yet, so can be rebroadcast
Message relayGetArbitraryDataFileListMessage = new GetArbitraryDataFileListMessage(signature, hashes, requestTime, requestHops, requestingPeer);
LOGGER.debug("Rebroadcasting hash list request from peer {} for signature {} to our other peers... totalRequestTime: {}, requestHops: {}", peer, Base58.encode(signature), totalRequestTime, requestHops);
Network.getInstance().broadcast(
broadcastPeer -> broadcastPeer == peer ||
Objects.equals(broadcastPeer.getPeerData().getAddress().getHost(), peer.getPeerData().getAddress().getHost())
? null : getArbitraryDataFileListMessage);
? null : relayGetArbitraryDataFileListMessage);
}
else {

View File

@@ -7,7 +7,6 @@ import org.qortal.controller.Controller;
import org.qortal.data.arbitrary.ArbitraryDirectConnectionInfo;
import org.qortal.data.arbitrary.ArbitraryFileListResponseInfo;
import org.qortal.data.arbitrary.ArbitraryRelayInfo;
import org.qortal.data.network.ArbitraryPeerData;
import org.qortal.data.network.PeerData;
import org.qortal.data.transaction.ArbitraryTransactionData;
import org.qortal.network.Network;
@@ -187,7 +186,7 @@ public class ArbitraryDataFileManager extends Thread {
ArbitraryDataFile existingFile = ArbitraryDataFile.fromHash(hash, signature);
boolean fileAlreadyExists = existingFile.exists();
String hash58 = Base58.encode(hash);
Message message = null;
ArbitraryDataFileMessage arbitraryDataFileMessage;
// Fetch the file if it doesn't exist locally
if (!fileAlreadyExists) {
@@ -195,10 +194,11 @@ public class ArbitraryDataFileManager extends Thread {
arbitraryDataFileRequests.put(hash58, NTP.getTime());
Message getArbitraryDataFileMessage = new GetArbitraryDataFileMessage(signature, hash);
Message response = null;
try {
message = peer.getResponseWithTimeout(getArbitraryDataFileMessage, (int) ArbitraryDataManager.ARBITRARY_REQUEST_TIMEOUT);
response = peer.getResponseWithTimeout(getArbitraryDataFileMessage, (int) ArbitraryDataManager.ARBITRARY_REQUEST_TIMEOUT);
} catch (InterruptedException e) {
// Will return below due to null message
// Will return below due to null response
}
arbitraryDataFileRequests.remove(hash58);
LOGGER.trace(String.format("Removed hash %.8s from arbitraryDataFileRequests", hash58));
@@ -206,22 +206,24 @@ public class ArbitraryDataFileManager extends Thread {
// We may need to remove the file list request, if we have all the files for this transaction
this.handleFileListRequests(signature);
if (message == null) {
LOGGER.debug("Received null message from peer {}", peer);
if (response == null) {
LOGGER.debug("Received null response from peer {}", peer);
return null;
}
if (message.getType() != Message.MessageType.ARBITRARY_DATA_FILE) {
LOGGER.debug("Received message with invalid type: {} from peer {}", message.getType(), peer);
if (response.getType() != MessageType.ARBITRARY_DATA_FILE) {
LOGGER.debug("Received response with invalid type: {} from peer {}", response.getType(), peer);
return null;
}
}
else {
ArbitraryDataFileMessage peersArbitraryDataFileMessage = (ArbitraryDataFileMessage) response;
arbitraryDataFileMessage = new ArbitraryDataFileMessage(signature, peersArbitraryDataFileMessage.getArbitraryDataFile());
} else {
LOGGER.debug(String.format("File hash %s already exists, so skipping the request", hash58));
arbitraryDataFileMessage = new ArbitraryDataFileMessage(signature, existingFile);
}
ArbitraryDataFileMessage arbitraryDataFileMessage = (ArbitraryDataFileMessage) message;
// We might want to forward the request to the peer that originally requested it
this.handleArbitraryDataFileForwarding(requestingPeer, message, originalMessage);
this.handleArbitraryDataFileForwarding(requestingPeer, arbitraryDataFileMessage, originalMessage);
boolean isRelayRequest = (requestingPeer != null);
if (isRelayRequest) {

View File

@@ -338,9 +338,11 @@ public class ArbitraryMetadataManager {
Peer requestingPeer = request.getB();
if (requestingPeer != null) {
ArbitraryMetadataMessage forwardArbitraryMetadataMessage = new ArbitraryMetadataMessage(signature, arbitraryMetadataMessage.getArbitraryMetadataFile());
// Forward to requesting peer
LOGGER.debug("Forwarding metadata to requesting peer: {}", requestingPeer);
if (!requestingPeer.sendMessage(arbitraryMetadataMessage)) {
if (!requestingPeer.sendMessage(forwardArbitraryMetadataMessage)) {
requestingPeer.disconnect("failed to forward arbitrary metadata");
}
}
@@ -423,8 +425,7 @@ public class ArbitraryMetadataManager {
// In relay mode - so ask our other peers if they have it
long requestTime = getArbitraryMetadataMessage.getRequestTime();
int requestHops = getArbitraryMetadataMessage.getRequestHops();
getArbitraryMetadataMessage.setRequestHops(++requestHops);
int requestHops = getArbitraryMetadataMessage.getRequestHops() + 1;
long totalRequestTime = now - requestTime;
if (totalRequestTime < RELAY_REQUEST_MAX_DURATION) {
@@ -432,11 +433,13 @@ public class ArbitraryMetadataManager {
if (requestHops < RELAY_REQUEST_MAX_HOPS) {
// Relay request hasn't reached the maximum number of hops yet, so can be rebroadcast
Message relayGetArbitraryMetadataMessage = new GetArbitraryMetadataMessage(signature, requestTime, requestHops);
LOGGER.debug("Rebroadcasting metadata request from peer {} for signature {} to our other peers... totalRequestTime: {}, requestHops: {}", peer, Base58.encode(signature), totalRequestTime, requestHops);
Network.getInstance().broadcast(
broadcastPeer -> broadcastPeer == peer ||
Objects.equals(broadcastPeer.getPeerData().getAddress().getHost(), peer.getPeerData().getAddress().getHost())
? null : getArbitraryMetadataMessage);
? null : relayGetArbitraryMetadataMessage);
}
else {

View File

@@ -107,7 +107,7 @@ public class NamesDatabaseIntegrityCheck {
BuyNameTransactionData buyNameTransactionData = (BuyNameTransactionData) currentTransaction;
Name nameObj = new Name(repository, buyNameTransactionData.getName());
if (nameObj != null && nameObj.getNameData() != null) {
nameObj.buy(buyNameTransactionData);
nameObj.buy(buyNameTransactionData, false);
modificationCount++;
LOGGER.trace("Processed BUY_NAME transaction for name {}", name);
}

View File

@@ -0,0 +1,885 @@
package org.qortal.controller.tradebot;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bitcoinj.core.*;
import org.bitcoinj.script.Script.ScriptType;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.account.PublicKeyAccount;
import org.qortal.api.model.crosschain.TradeBotCreateRequest;
import org.qortal.asset.Asset;
import org.qortal.crosschain.*;
import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData;
import org.qortal.data.crosschain.CrossChainTradeData;
import org.qortal.data.crosschain.TradeBotData;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.transaction.Transaction.ValidationResult;
import org.qortal.transform.TransformationException;
import org.qortal.transform.transaction.DeployAtTransactionTransformer;
import org.qortal.utils.Base58;
import org.qortal.utils.NTP;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toMap;
/**
* Performing cross-chain trading steps on behalf of user.
* <p>
* We deal with three different independent state-spaces here:
* <ul>
* <li>Qortal blockchain</li>
* <li>Foreign blockchain</li>
* <li>Trade-bot entries</li>
* </ul>
*/
public class DigibyteACCTv3TradeBot implements AcctTradeBot {
private static final Logger LOGGER = LogManager.getLogger(DigibyteACCTv3TradeBot.class);
public enum State implements TradeBot.StateNameAndValueSupplier {
BOB_WAITING_FOR_AT_CONFIRM(10, false, false),
BOB_WAITING_FOR_MESSAGE(15, true, true),
BOB_WAITING_FOR_AT_REDEEM(25, true, true),
BOB_DONE(30, false, false),
BOB_REFUNDED(35, false, false),
ALICE_WAITING_FOR_AT_LOCK(85, true, true),
ALICE_DONE(95, false, false),
ALICE_REFUNDING_A(105, true, true),
ALICE_REFUNDED(110, false, false);
private static final Map<Integer, State> map = stream(State.values()).collect(toMap(state -> state.value, state -> state));
public final int value;
public final boolean requiresAtData;
public final boolean requiresTradeData;
State(int value, boolean requiresAtData, boolean requiresTradeData) {
this.value = value;
this.requiresAtData = requiresAtData;
this.requiresTradeData = requiresTradeData;
}
public static State valueOf(int value) {
return map.get(value);
}
@Override
public String getState() {
return this.name();
}
@Override
public int getStateValue() {
return this.value;
}
}
/** Maximum time Bob waits for his AT creation transaction to be confirmed into a block. (milliseconds) */
private static final long MAX_AT_CONFIRMATION_PERIOD = 24 * 60 * 60 * 1000L; // ms
private static DigibyteACCTv3TradeBot instance;
private final List<String> endStates = Arrays.asList(State.BOB_DONE, State.BOB_REFUNDED, State.ALICE_DONE, State.ALICE_REFUNDING_A, State.ALICE_REFUNDED).stream()
.map(State::name)
.collect(Collectors.toUnmodifiableList());
private DigibyteACCTv3TradeBot() {
}
public static synchronized DigibyteACCTv3TradeBot getInstance() {
if (instance == null)
instance = new DigibyteACCTv3TradeBot();
return instance;
}
@Override
public List<String> getEndStates() {
return this.endStates;
}
/**
* Creates a new trade-bot entry from the "Bob" viewpoint, i.e. OFFERing QORT in exchange for DGB.
* <p>
* Generates:
* <ul>
* <li>new 'trade' private key</li>
* </ul>
* Derives:
* <ul>
* <li>'native' (as in Qortal) public key, public key hash, address (starting with Q)</li>
* <li>'foreign' (as in Digibyte) public key, public key hash</li>
* </ul>
* A Qortal AT is then constructed including the following as constants in the 'data segment':
* <ul>
* <li>'native'/Qortal 'trade' address - used as a MESSAGE contact</li>
* <li>'foreign'/Digibyte public key hash - used by Alice's P2SH scripts to allow redeem</li>
* <li>QORT amount on offer by Bob</li>
* <li>DGB amount expected in return by Bob (from Alice)</li>
* <li>trading timeout, in case things go wrong and everyone needs to refund</li>
* </ul>
* Returns a DEPLOY_AT transaction that needs to be signed and broadcast to the Qortal network.
* <p>
* Trade-bot will wait for Bob's AT to be deployed before taking next step.
* <p>
* @param repository
* @param tradeBotCreateRequest
* @return raw, unsigned DEPLOY_AT transaction
* @throws DataException
*/
public byte[] createTrade(Repository repository, TradeBotCreateRequest tradeBotCreateRequest) throws DataException {
byte[] tradePrivateKey = TradeBot.generateTradePrivateKey();
byte[] tradeNativePublicKey = TradeBot.deriveTradeNativePublicKey(tradePrivateKey);
byte[] tradeNativePublicKeyHash = Crypto.hash160(tradeNativePublicKey);
String tradeNativeAddress = Crypto.toAddress(tradeNativePublicKey);
byte[] tradeForeignPublicKey = TradeBot.deriveTradeForeignPublicKey(tradePrivateKey);
byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
// Convert Digibyte receiving address into public key hash (we only support P2PKH at this time)
Address digibyteReceivingAddress;
try {
digibyteReceivingAddress = Address.fromString(Digibyte.getInstance().getNetworkParameters(), tradeBotCreateRequest.receivingAddress);
} catch (AddressFormatException e) {
throw new DataException("Unsupported Digibyte receiving address: " + tradeBotCreateRequest.receivingAddress);
}
if (digibyteReceivingAddress.getOutputScriptType() != ScriptType.P2PKH)
throw new DataException("Unsupported Digibyte receiving address: " + tradeBotCreateRequest.receivingAddress);
byte[] digibyteReceivingAccountInfo = digibyteReceivingAddress.getHash();
PublicKeyAccount creator = new PublicKeyAccount(repository, tradeBotCreateRequest.creatorPublicKey);
// Deploy AT
long timestamp = NTP.getTime();
byte[] reference = creator.getLastReference();
long fee = 0L;
byte[] signature = null;
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, creator.getPublicKey(), fee, signature);
String name = "QORT/DGB ACCT";
String description = "QORT/DGB cross-chain trade";
String aTType = "ACCT";
String tags = "ACCT QORT DGB";
byte[] creationBytes = DigibyteACCTv3.buildQortalAT(tradeNativeAddress, tradeForeignPublicKeyHash, tradeBotCreateRequest.qortAmount,
tradeBotCreateRequest.foreignAmount, tradeBotCreateRequest.tradeTimeout);
long amount = tradeBotCreateRequest.fundingQortAmount;
DeployAtTransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, aTType, tags, creationBytes, amount, Asset.QORT);
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
fee = deployAtTransaction.calcRecommendedFee();
deployAtTransactionData.setFee(fee);
DeployAtTransaction.ensureATAddress(deployAtTransactionData);
String atAddress = deployAtTransactionData.getAtAddress();
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, DigibyteACCTv3.NAME,
State.BOB_WAITING_FOR_AT_CONFIRM.name(), State.BOB_WAITING_FOR_AT_CONFIRM.value,
creator.getAddress(), atAddress, timestamp, tradeBotCreateRequest.qortAmount,
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
null, null,
SupportedBlockchain.DIGIBYTE.name(),
tradeForeignPublicKey, tradeForeignPublicKeyHash,
tradeBotCreateRequest.foreignAmount, null, null, null, digibyteReceivingAccountInfo);
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Built AT %s. Waiting for deployment", atAddress));
// Attempt to backup the trade bot data
TradeBot.backupTradeBotData(repository, null);
// Return to user for signing and broadcast as we don't have their Qortal private key
try {
return DeployAtTransactionTransformer.toBytes(deployAtTransactionData);
} catch (TransformationException e) {
throw new DataException("Failed to transform DEPLOY_AT transaction?", e);
}
}
/**
* Creates a trade-bot entry from the 'Alice' viewpoint, i.e. matching DGB to an existing offer.
* <p>
* Requires a chosen trade offer from Bob, passed by <tt>crossChainTradeData</tt>
* and access to a Digibyte wallet via <tt>xprv58</tt>.
* <p>
* The <tt>crossChainTradeData</tt> contains the current trade offer state
* as extracted from the AT's data segment.
* <p>
* Access to a funded wallet is via a Digibyte BIP32 hierarchical deterministic key,
* passed via <tt>xprv58</tt>.
* <b>This key will be stored in your node's database</b>
* to allow trade-bot to create/fund the necessary P2SH transactions!
* However, due to the nature of BIP32 keys, it is possible to give the trade-bot
* only a subset of wallet access (see BIP32 for more details).
* <p>
* As an example, the xprv58 can be extract from a <i>legacy, password-less</i>
* Electrum wallet by going to the console tab and entering:<br>
* <tt>wallet.keystore.xprv</tt><br>
* which should result in a base58 string starting with either 'xprv' (for Digibyte main-net)
* or 'tprv' for (Digibyte test-net).
* <p>
* It is envisaged that the value in <tt>xprv58</tt> will actually come from a Qortal-UI-managed wallet.
* <p>
* If sufficient funds are available, <b>this method will actually fund the P2SH-A</b>
* with the Digibyte amount expected by 'Bob'.
* <p>
* If the Digibyte transaction is successfully broadcast to the network then
* we also send a MESSAGE to Bob's trade-bot to let them know.
* <p>
* The trade-bot entry is saved to the repository and the cross-chain trading process commences.
* <p>
* @param repository
* @param crossChainTradeData chosen trade OFFER that Alice wants to match
* @param xprv58 funded wallet xprv in base58
* @return true if P2SH-A funding transaction successfully broadcast to Digibyte network, false otherwise
* @throws DataException
*/
public ResponseResult startResponse(Repository repository, ATData atData, ACCT acct, CrossChainTradeData crossChainTradeData, String xprv58, String receivingAddress) throws DataException {
byte[] tradePrivateKey = TradeBot.generateTradePrivateKey();
byte[] secretA = TradeBot.generateSecret();
byte[] hashOfSecretA = Crypto.hash160(secretA);
byte[] tradeNativePublicKey = TradeBot.deriveTradeNativePublicKey(tradePrivateKey);
byte[] tradeNativePublicKeyHash = Crypto.hash160(tradeNativePublicKey);
String tradeNativeAddress = Crypto.toAddress(tradeNativePublicKey);
byte[] tradeForeignPublicKey = TradeBot.deriveTradeForeignPublicKey(tradePrivateKey);
byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
byte[] receivingPublicKeyHash = Base58.decode(receivingAddress); // Actually the whole address, not just PKH
// We need to generate lockTime-A: add tradeTimeout to now
long now = NTP.getTime();
int lockTimeA = crossChainTradeData.tradeTimeout * 60 + (int) (now / 1000L);
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, DigibyteACCTv3.NAME,
State.ALICE_WAITING_FOR_AT_LOCK.name(), State.ALICE_WAITING_FOR_AT_LOCK.value,
receivingAddress, crossChainTradeData.qortalAtAddress, now, crossChainTradeData.qortAmount,
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
secretA, hashOfSecretA,
SupportedBlockchain.DIGIBYTE.name(),
tradeForeignPublicKey, tradeForeignPublicKeyHash,
crossChainTradeData.expectedForeignAmount, xprv58, null, lockTimeA, receivingPublicKeyHash);
// Attempt to backup the trade bot data
// Include tradeBotData as an additional parameter, since it's not in the repository yet
TradeBot.backupTradeBotData(repository, Arrays.asList(tradeBotData));
// Check we have enough funds via xprv58 to fund P2SH to cover expectedForeignAmount
long p2shFee;
try {
p2shFee = Digibyte.getInstance().getP2shFee(now);
} catch (ForeignBlockchainException e) {
LOGGER.debug("Couldn't estimate Digibyte fees?");
return ResponseResult.NETWORK_ISSUE;
}
// Fee for redeem/refund is subtracted from P2SH-A balance.
// Do not include fee for funding transaction as this is covered by buildSpend()
long amountA = crossChainTradeData.expectedForeignAmount + p2shFee /*redeeming/refunding P2SH-A*/;
// P2SH-A to be funded
byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(tradeForeignPublicKeyHash, lockTimeA, crossChainTradeData.creatorForeignPKH, hashOfSecretA);
String p2shAddress = Digibyte.getInstance().deriveP2shAddress(redeemScriptBytes);
// Build transaction for funding P2SH-A
Transaction p2shFundingTransaction = Digibyte.getInstance().buildSpend(tradeBotData.getForeignKey(), p2shAddress, amountA);
if (p2shFundingTransaction == null) {
LOGGER.debug("Unable to build P2SH-A funding transaction - lack of funds?");
return ResponseResult.BALANCE_ISSUE;
}
try {
Digibyte.getInstance().broadcastTransaction(p2shFundingTransaction);
} catch (ForeignBlockchainException e) {
// We couldn't fund P2SH-A at this time
LOGGER.debug("Couldn't broadcast P2SH-A funding transaction?");
return ResponseResult.NETWORK_ISSUE;
}
// Attempt to send MESSAGE to Bob's Qortal trade address
byte[] messageData = DigibyteACCTv3.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA());
String messageRecipient = crossChainTradeData.qortalCreatorTradeAddress;
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
if (!isMessageAlreadySent) {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
messageTransaction.computeNonce();
messageTransaction.sign(sender);
// reset repository state to prevent deadlock
repository.discardChanges();
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
return ResponseResult.NETWORK_ISSUE;
}
}
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress));
return ResponseResult.OK;
}
@Override
public boolean canDelete(Repository repository, TradeBotData tradeBotData) throws DataException {
State tradeBotState = State.valueOf(tradeBotData.getStateValue());
if (tradeBotState == null)
return true;
// If the AT doesn't exist then we might as well let the user tidy up
if (!repository.getATRepository().exists(tradeBotData.getAtAddress()))
return true;
switch (tradeBotState) {
case BOB_WAITING_FOR_AT_CONFIRM:
case ALICE_DONE:
case BOB_DONE:
case ALICE_REFUNDED:
case BOB_REFUNDED:
case ALICE_REFUNDING_A:
return true;
default:
return false;
}
}
@Override
public void progress(Repository repository, TradeBotData tradeBotData) throws DataException, ForeignBlockchainException {
State tradeBotState = State.valueOf(tradeBotData.getStateValue());
if (tradeBotState == null) {
LOGGER.info(() -> String.format("Trade-bot entry for AT %s has invalid state?", tradeBotData.getAtAddress()));
return;
}
ATData atData = null;
CrossChainTradeData tradeData = null;
if (tradeBotState.requiresAtData) {
// Attempt to fetch AT data
atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress());
if (atData == null) {
LOGGER.debug(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress()));
return;
}
if (tradeBotState.requiresTradeData) {
tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
if (tradeData == null) {
LOGGER.warn(() -> String.format("Unable to fetch ACCT trade data for AT %s from repository", tradeBotData.getAtAddress()));
return;
}
}
}
switch (tradeBotState) {
case BOB_WAITING_FOR_AT_CONFIRM:
handleBobWaitingForAtConfirm(repository, tradeBotData);
break;
case BOB_WAITING_FOR_MESSAGE:
TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
handleBobWaitingForMessage(repository, tradeBotData, atData, tradeData);
break;
case ALICE_WAITING_FOR_AT_LOCK:
TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
handleAliceWaitingForAtLock(repository, tradeBotData, atData, tradeData);
break;
case BOB_WAITING_FOR_AT_REDEEM:
TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
handleBobWaitingForAtRedeem(repository, tradeBotData, atData, tradeData);
break;
case ALICE_DONE:
case BOB_DONE:
break;
case ALICE_REFUNDING_A:
TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
handleAliceRefundingP2shA(repository, tradeBotData, atData, tradeData);
break;
case ALICE_REFUNDED:
case BOB_REFUNDED:
break;
}
}
/**
* Trade-bot is waiting for Bob's AT to deploy.
* <p>
* If AT is deployed, then trade-bot's next step is to wait for MESSAGE from Alice.
*/
private void handleBobWaitingForAtConfirm(Repository repository, TradeBotData tradeBotData) throws DataException {
if (!repository.getATRepository().exists(tradeBotData.getAtAddress())) {
if (NTP.getTime() - tradeBotData.getTimestamp() <= MAX_AT_CONFIRMATION_PERIOD)
return;
// We've waited ages for AT to be confirmed into a block but something has gone awry.
// After this long we assume transaction loss so give up with trade-bot entry too.
tradeBotData.setState(State.BOB_REFUNDED.name());
tradeBotData.setStateValue(State.BOB_REFUNDED.value);
tradeBotData.setTimestamp(NTP.getTime());
// We delete trade-bot entry here instead of saving, hence not using updateTradeBotState()
repository.getCrossChainRepository().delete(tradeBotData.getTradePrivateKey());
repository.saveChanges();
LOGGER.info(() -> String.format("AT %s never confirmed. Giving up on trade", tradeBotData.getAtAddress()));
TradeBot.notifyStateChange(tradeBotData);
return;
}
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_WAITING_FOR_MESSAGE,
() -> String.format("AT %s confirmed ready. Waiting for trade message", tradeBotData.getAtAddress()));
}
/**
* Trade-bot is waiting for MESSAGE from Alice's trade-bot, containing Alice's trade info.
* <p>
* It's possible Bob has cancelling his trade offer, receiving an automatic QORT refund,
* in which case trade-bot is done with this specific trade and finalizes on refunded state.
* <p>
* Assuming trade is still on offer, trade-bot checks the contents of MESSAGE from Alice's trade-bot.
* <p>
* Details from Alice are used to derive P2SH-A address and this is checked for funding balance.
* <p>
* Assuming P2SH-A has at least expected Digibyte balance,
* Bob's trade-bot constructs a zero-fee, PoW MESSAGE to send to Bob's AT with more trade details.
* <p>
* On processing this MESSAGE, Bob's AT should switch into 'TRADE' mode and only trade with Alice.
* <p>
* Trade-bot's next step is to wait for Alice to redeem the AT, which will allow Bob to
* extract secret-A needed to redeem Alice's P2SH.
* @throws ForeignBlockchainException
*/
private void handleBobWaitingForMessage(Repository repository, TradeBotData tradeBotData,
ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
// If AT has finished then Bob likely cancelled his trade offer
if (atData.getIsFinished()) {
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED,
() -> String.format("AT %s cancelled - trading aborted", tradeBotData.getAtAddress()));
return;
}
Digibyte digibyte = Digibyte.getInstance();
String address = tradeBotData.getTradeNativeAddress();
List<MessageTransactionData> messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, address, null, null, null);
for (MessageTransactionData messageTransactionData : messageTransactionsData) {
if (messageTransactionData.isText())
continue;
// We're expecting: HASH160(secret-A), Alice's Digibyte pubkeyhash and lockTime-A
byte[] messageData = messageTransactionData.getData();
DigibyteACCTv3.OfferMessageData offerMessageData = DigibyteACCTv3.extractOfferMessageData(messageData);
if (offerMessageData == null)
continue;
byte[] aliceForeignPublicKeyHash = offerMessageData.partnerDigibytePKH;
byte[] hashOfSecretA = offerMessageData.hashOfSecretA;
int lockTimeA = (int) offerMessageData.lockTimeA;
long messageTimestamp = messageTransactionData.getTimestamp();
int refundTimeout = DigibyteACCTv3.calcRefundTimeout(messageTimestamp, lockTimeA);
// Determine P2SH-A address and confirm funded
byte[] redeemScriptA = BitcoinyHTLC.buildScript(aliceForeignPublicKeyHash, lockTimeA, tradeBotData.getTradeForeignPublicKeyHash(), hashOfSecretA);
String p2shAddressA = digibyte.deriveP2shAddress(redeemScriptA);
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
long p2shFee = Digibyte.getInstance().getP2shFee(feeTimestamp);
final long minimumAmountA = tradeBotData.getForeignAmount() + p2shFee;
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(digibyte.getBlockchainProvider(), p2shAddressA, minimumAmountA);
switch (htlcStatusA) {
case UNFUNDED:
case FUNDING_IN_PROGRESS:
// There might be another MESSAGE from someone else with an actually funded P2SH-A...
continue;
case REDEEM_IN_PROGRESS:
case REDEEMED:
// We've already redeemed this?
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_DONE,
() -> String.format("P2SH-A %s already spent? Assuming trade complete", p2shAddressA));
return;
case REFUND_IN_PROGRESS:
case REFUNDED:
// This P2SH-A is burnt, but there might be another MESSAGE from someone else with an actually funded P2SH-A...
continue;
case FUNDED:
// Fall-through out of switch...
break;
}
// Good to go - send MESSAGE to AT
String aliceNativeAddress = Crypto.toAddress(messageTransactionData.getCreatorPublicKey());
// Build outgoing message, padding each part to 32 bytes to make it easier for AT to consume
byte[] outgoingMessageData = DigibyteACCTv3.buildTradeMessage(aliceNativeAddress, aliceForeignPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
String messageRecipient = tradeBotData.getAtAddress();
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, outgoingMessageData);
if (!isMessageAlreadySent) {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false);
outgoingMessageTransaction.computeNonce();
outgoingMessageTransaction.sign(sender);
// reset repository state to prevent deadlock
repository.discardChanges();
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
return;
}
}
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_WAITING_FOR_AT_REDEEM,
() -> String.format("Locked AT %s to %s. Waiting for AT redeem", tradeBotData.getAtAddress(), aliceNativeAddress));
return;
}
}
/**
* Trade-bot is waiting for Bob's AT to switch to TRADE mode and lock trade to Alice only.
* <p>
* It's possible that Bob has cancelled his trade offer in the mean time, or that somehow
* this process has taken so long that we've reached P2SH-A's locktime, or that someone else
* has managed to trade with Bob. In any of these cases, trade-bot switches to begin the refunding process.
* <p>
* Assuming Bob's AT is locked to Alice, trade-bot checks AT's state data to make sure it is correct.
* <p>
* If all is well, trade-bot then redeems AT using Alice's secret-A, releasing Bob's QORT to Alice.
* <p>
* In revealing a valid secret-A, Bob can then redeem the DGB funds from P2SH-A.
* <p>
* @throws ForeignBlockchainException
*/
private void handleAliceWaitingForAtLock(Repository repository, TradeBotData tradeBotData,
ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
if (aliceUnexpectedState(repository, tradeBotData, atData, crossChainTradeData))
return;
Digibyte digibyte = Digibyte.getInstance();
int lockTimeA = tradeBotData.getLockTimeA();
// Refund P2SH-A if we've passed lockTime-A
if (NTP.getTime() >= lockTimeA * 1000L) {
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
String p2shAddressA = digibyte.deriveP2shAddress(redeemScriptA);
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
long p2shFee = Digibyte.getInstance().getP2shFee(feeTimestamp);
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(digibyte.getBlockchainProvider(), p2shAddressA, minimumAmountA);
switch (htlcStatusA) {
case UNFUNDED:
case FUNDING_IN_PROGRESS:
case FUNDED:
break;
case REDEEM_IN_PROGRESS:
case REDEEMED:
// Already redeemed?
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
() -> String.format("P2SH-A %s already spent? Assuming trade completed", p2shAddressA));
return;
case REFUND_IN_PROGRESS:
case REFUNDED:
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED,
() -> String.format("P2SH-A %s already refunded. Trade aborted", p2shAddressA));
return;
}
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
() -> atData.getIsFinished()
? String.format("AT %s cancelled. Refunding P2SH-A %s - aborting trade", tradeBotData.getAtAddress(), p2shAddressA)
: String.format("LockTime-A reached, refunding P2SH-A %s - aborting trade", p2shAddressA));
return;
}
// We're waiting for AT to be in TRADE mode
if (crossChainTradeData.mode != AcctMode.TRADING)
return;
// AT is in TRADE mode and locked to us as checked by aliceUnexpectedState() above
// Find our MESSAGE to AT from previous state
List<MessageTransactionData> messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(tradeBotData.getTradeNativePublicKey(),
crossChainTradeData.qortalCreatorTradeAddress, null, null, null);
if (messageTransactionsData == null || messageTransactionsData.isEmpty()) {
LOGGER.warn(() -> String.format("Unable to find our message to trade creator %s?", crossChainTradeData.qortalCreatorTradeAddress));
return;
}
long recipientMessageTimestamp = messageTransactionsData.get(0).getTimestamp();
int refundTimeout = DigibyteACCTv3.calcRefundTimeout(recipientMessageTimestamp, lockTimeA);
// Our calculated refundTimeout should match AT's refundTimeout
if (refundTimeout != crossChainTradeData.refundTimeout) {
LOGGER.debug(() -> String.format("Trade AT refundTimeout '%d' doesn't match our refundTimeout '%d'", crossChainTradeData.refundTimeout, refundTimeout));
// We'll eventually refund
return;
}
// We're good to redeem AT
// Send 'redeem' MESSAGE to AT using both secret
byte[] secretA = tradeBotData.getSecret();
String qortalReceivingAddress = Base58.encode(tradeBotData.getReceivingAccountInfo()); // Actually contains whole address, not just PKH
byte[] messageData = DigibyteACCTv3.buildRedeemMessage(secretA, qortalReceivingAddress);
String messageRecipient = tradeBotData.getAtAddress();
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
if (!isMessageAlreadySent) {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
messageTransaction.computeNonce();
messageTransaction.sign(sender);
// Reset repository state to prevent deadlock
repository.discardChanges();
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
return;
}
}
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
() -> String.format("Redeeming AT %s. Funds should arrive at %s",
tradeBotData.getAtAddress(), qortalReceivingAddress));
}
/**
* Trade-bot is waiting for Alice to redeem Bob's AT, thus revealing secret-A which is required to spend the DGB funds from P2SH-A.
* <p>
* It's possible that Bob's AT has reached its trading timeout and automatically refunded QORT back to Bob. In which case,
* trade-bot is done with this specific trade and finalizes in refunded state.
* <p>
* Assuming trade-bot can extract a valid secret-A from Alice's MESSAGE then trade-bot uses that to redeem the DGB funds from P2SH-A
* to Bob's 'foreign'/Digibyte trade legacy-format address, as derived from trade private key.
* <p>
* (This could potentially be 'improved' to send DGB to any address of Bob's choosing by changing the transaction output).
* <p>
* If trade-bot successfully broadcasts the transaction, then this specific trade is done.
* @throws ForeignBlockchainException
*/
private void handleBobWaitingForAtRedeem(Repository repository, TradeBotData tradeBotData,
ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
// AT should be 'finished' once Alice has redeemed QORT funds
if (!atData.getIsFinished())
// Not finished yet
return;
// If AT is REFUNDED or CANCELLED then something has gone wrong
if (crossChainTradeData.mode == AcctMode.REFUNDED || crossChainTradeData.mode == AcctMode.CANCELLED) {
// Alice hasn't redeemed the QORT, so there is no point in trying to redeem the DGB
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED,
() -> String.format("AT %s has auto-refunded - trade aborted", tradeBotData.getAtAddress()));
return;
}
byte[] secretA = DigibyteACCTv3.getInstance().findSecretA(repository, crossChainTradeData);
if (secretA == null) {
LOGGER.debug(() -> String.format("Unable to find secret-A from redeem message to AT %s?", tradeBotData.getAtAddress()));
return;
}
// Use secret-A to redeem P2SH-A
Digibyte digibyte = Digibyte.getInstance();
byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo();
int lockTimeA = crossChainTradeData.lockTimeA;
byte[] redeemScriptA = BitcoinyHTLC.buildScript(crossChainTradeData.partnerForeignPKH, lockTimeA, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretA);
String p2shAddressA = digibyte.deriveP2shAddress(redeemScriptA);
// Fee for redeem/refund is subtracted from P2SH-A balance.
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
long p2shFee = Digibyte.getInstance().getP2shFee(feeTimestamp);
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(digibyte.getBlockchainProvider(), p2shAddressA, minimumAmountA);
switch (htlcStatusA) {
case UNFUNDED:
case FUNDING_IN_PROGRESS:
// P2SH-A suddenly not funded? Our best bet at this point is to hope for AT auto-refund
return;
case REDEEM_IN_PROGRESS:
case REDEEMED:
// Double-check that we have redeemed P2SH-A...
break;
case REFUND_IN_PROGRESS:
case REFUNDED:
// Wait for AT to auto-refund
return;
case FUNDED: {
Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
List<TransactionOutput> fundingOutputs = digibyte.getUnspentOutputs(p2shAddressA);
Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(digibyte.getNetworkParameters(), redeemAmount, redeemKey,
fundingOutputs, redeemScriptA, secretA, receivingAccountInfo);
digibyte.broadcastTransaction(p2shRedeemTransaction);
break;
}
}
String receivingAddress = digibyte.pkhToAddress(receivingAccountInfo);
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_DONE,
() -> String.format("P2SH-A %s redeemed. Funds should arrive at %s", tradeBotData.getAtAddress(), receivingAddress));
}
/**
* Trade-bot is attempting to refund P2SH-A.
* @throws ForeignBlockchainException
*/
private void handleAliceRefundingP2shA(Repository repository, TradeBotData tradeBotData,
ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
int lockTimeA = tradeBotData.getLockTimeA();
// We can't refund P2SH-A until lockTime-A has passed
if (NTP.getTime() <= lockTimeA * 1000L)
return;
Digibyte digibyte = Digibyte.getInstance();
// We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
int medianBlockTime = digibyte.getMedianBlockTime();
if (medianBlockTime <= lockTimeA)
return;
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
String p2shAddressA = digibyte.deriveP2shAddress(redeemScriptA);
// Fee for redeem/refund is subtracted from P2SH-A balance.
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
long p2shFee = Digibyte.getInstance().getP2shFee(feeTimestamp);
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(digibyte.getBlockchainProvider(), p2shAddressA, minimumAmountA);
switch (htlcStatusA) {
case UNFUNDED:
case FUNDING_IN_PROGRESS:
// Still waiting for P2SH-A to be funded...
return;
case REDEEM_IN_PROGRESS:
case REDEEMED:
// Too late!
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
() -> String.format("P2SH-A %s already spent!", p2shAddressA));
return;
case REFUND_IN_PROGRESS:
case REFUNDED:
break;
case FUNDED:{
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
List<TransactionOutput> fundingOutputs = digibyte.getUnspentOutputs(p2shAddressA);
// Determine receive address for refund
String receiveAddress = digibyte.getUnusedReceiveAddress(tradeBotData.getForeignKey());
Address receiving = Address.fromString(digibyte.getNetworkParameters(), receiveAddress);
Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(digibyte.getNetworkParameters(), refundAmount, refundKey,
fundingOutputs, redeemScriptA, lockTimeA, receiving.getHash());
digibyte.broadcastTransaction(p2shRefundTransaction);
break;
}
}
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED,
() -> String.format("LockTime-A reached. Refunded P2SH-A %s. Trade aborted", p2shAddressA));
}
/**
* Returns true if Alice finds AT unexpectedly cancelled, refunded, redeemed or locked to someone else.
* <p>
* Will automatically update trade-bot state to <tt>ALICE_REFUNDING_A</tt> or <tt>ALICE_DONE</tt> as necessary.
*
* @throws DataException
* @throws ForeignBlockchainException
*/
private boolean aliceUnexpectedState(Repository repository, TradeBotData tradeBotData,
ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
// This is OK
if (!atData.getIsFinished() && crossChainTradeData.mode == AcctMode.OFFERING)
return false;
boolean isAtLockedToUs = tradeBotData.getTradeNativeAddress().equals(crossChainTradeData.qortalPartnerAddress);
if (!atData.getIsFinished() && crossChainTradeData.mode == AcctMode.TRADING)
if (isAtLockedToUs) {
// AT is trading with us - OK
return false;
} else {
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
() -> String.format("AT %s trading with someone else: %s. Refunding & aborting trade", tradeBotData.getAtAddress(), crossChainTradeData.qortalPartnerAddress));
return true;
}
if (atData.getIsFinished() && crossChainTradeData.mode == AcctMode.REDEEMED && isAtLockedToUs) {
// We've redeemed already?
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
() -> String.format("AT %s already redeemed by us. Trade completed", tradeBotData.getAtAddress()));
} else {
// Any other state is not good, so start defensive refund
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
() -> String.format("AT %s cancelled/refunded/redeemed by someone else/invalid state. Refunding & aborting trade", tradeBotData.getAtAddress()));
}
return true;
}
private long calcFeeTimestamp(int lockTimeA, int tradeTimeout) {
return (lockTimeA - tradeTimeout * 60) * 1000L;
}
}

View File

@@ -0,0 +1,885 @@
package org.qortal.controller.tradebot;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bitcoinj.core.*;
import org.bitcoinj.script.Script.ScriptType;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.account.PublicKeyAccount;
import org.qortal.api.model.crosschain.TradeBotCreateRequest;
import org.qortal.asset.Asset;
import org.qortal.crosschain.*;
import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData;
import org.qortal.data.crosschain.CrossChainTradeData;
import org.qortal.data.crosschain.TradeBotData;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.transaction.Transaction.ValidationResult;
import org.qortal.transform.TransformationException;
import org.qortal.transform.transaction.DeployAtTransactionTransformer;
import org.qortal.utils.Base58;
import org.qortal.utils.NTP;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toMap;
/**
* Performing cross-chain trading steps on behalf of user.
* <p>
* We deal with three different independent state-spaces here:
* <ul>
* <li>Qortal blockchain</li>
* <li>Foreign blockchain</li>
* <li>Trade-bot entries</li>
* </ul>
*/
public class RavencoinACCTv3TradeBot implements AcctTradeBot {
private static final Logger LOGGER = LogManager.getLogger(RavencoinACCTv3TradeBot.class);
public enum State implements TradeBot.StateNameAndValueSupplier {
BOB_WAITING_FOR_AT_CONFIRM(10, false, false),
BOB_WAITING_FOR_MESSAGE(15, true, true),
BOB_WAITING_FOR_AT_REDEEM(25, true, true),
BOB_DONE(30, false, false),
BOB_REFUNDED(35, false, false),
ALICE_WAITING_FOR_AT_LOCK(85, true, true),
ALICE_DONE(95, false, false),
ALICE_REFUNDING_A(105, true, true),
ALICE_REFUNDED(110, false, false);
private static final Map<Integer, State> map = stream(State.values()).collect(toMap(state -> state.value, state -> state));
public final int value;
public final boolean requiresAtData;
public final boolean requiresTradeData;
State(int value, boolean requiresAtData, boolean requiresTradeData) {
this.value = value;
this.requiresAtData = requiresAtData;
this.requiresTradeData = requiresTradeData;
}
public static State valueOf(int value) {
return map.get(value);
}
@Override
public String getState() {
return this.name();
}
@Override
public int getStateValue() {
return this.value;
}
}
/** Maximum time Bob waits for his AT creation transaction to be confirmed into a block. (milliseconds) */
private static final long MAX_AT_CONFIRMATION_PERIOD = 24 * 60 * 60 * 1000L; // ms
private static RavencoinACCTv3TradeBot instance;
private final List<String> endStates = Arrays.asList(State.BOB_DONE, State.BOB_REFUNDED, State.ALICE_DONE, State.ALICE_REFUNDING_A, State.ALICE_REFUNDED).stream()
.map(State::name)
.collect(Collectors.toUnmodifiableList());
private RavencoinACCTv3TradeBot() {
}
public static synchronized RavencoinACCTv3TradeBot getInstance() {
if (instance == null)
instance = new RavencoinACCTv3TradeBot();
return instance;
}
@Override
public List<String> getEndStates() {
return this.endStates;
}
/**
* Creates a new trade-bot entry from the "Bob" viewpoint, i.e. OFFERing QORT in exchange for RVN.
* <p>
* Generates:
* <ul>
* <li>new 'trade' private key</li>
* </ul>
* Derives:
* <ul>
* <li>'native' (as in Qortal) public key, public key hash, address (starting with Q)</li>
* <li>'foreign' (as in Ravencoin) public key, public key hash</li>
* </ul>
* A Qortal AT is then constructed including the following as constants in the 'data segment':
* <ul>
* <li>'native'/Qortal 'trade' address - used as a MESSAGE contact</li>
* <li>'foreign'/Ravencoin public key hash - used by Alice's P2SH scripts to allow redeem</li>
* <li>QORT amount on offer by Bob</li>
* <li>RVN amount expected in return by Bob (from Alice)</li>
* <li>trading timeout, in case things go wrong and everyone needs to refund</li>
* </ul>
* Returns a DEPLOY_AT transaction that needs to be signed and broadcast to the Qortal network.
* <p>
* Trade-bot will wait for Bob's AT to be deployed before taking next step.
* <p>
* @param repository
* @param tradeBotCreateRequest
* @return raw, unsigned DEPLOY_AT transaction
* @throws DataException
*/
public byte[] createTrade(Repository repository, TradeBotCreateRequest tradeBotCreateRequest) throws DataException {
byte[] tradePrivateKey = TradeBot.generateTradePrivateKey();
byte[] tradeNativePublicKey = TradeBot.deriveTradeNativePublicKey(tradePrivateKey);
byte[] tradeNativePublicKeyHash = Crypto.hash160(tradeNativePublicKey);
String tradeNativeAddress = Crypto.toAddress(tradeNativePublicKey);
byte[] tradeForeignPublicKey = TradeBot.deriveTradeForeignPublicKey(tradePrivateKey);
byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
// Convert Ravencoin receiving address into public key hash (we only support P2PKH at this time)
Address ravencoinReceivingAddress;
try {
ravencoinReceivingAddress = Address.fromString(Ravencoin.getInstance().getNetworkParameters(), tradeBotCreateRequest.receivingAddress);
} catch (AddressFormatException e) {
throw new DataException("Unsupported Ravencoin receiving address: " + tradeBotCreateRequest.receivingAddress);
}
if (ravencoinReceivingAddress.getOutputScriptType() != ScriptType.P2PKH)
throw new DataException("Unsupported Ravencoin receiving address: " + tradeBotCreateRequest.receivingAddress);
byte[] ravencoinReceivingAccountInfo = ravencoinReceivingAddress.getHash();
PublicKeyAccount creator = new PublicKeyAccount(repository, tradeBotCreateRequest.creatorPublicKey);
// Deploy AT
long timestamp = NTP.getTime();
byte[] reference = creator.getLastReference();
long fee = 0L;
byte[] signature = null;
BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, creator.getPublicKey(), fee, signature);
String name = "QORT/RVN ACCT";
String description = "QORT/RVN cross-chain trade";
String aTType = "ACCT";
String tags = "ACCT QORT RVN";
byte[] creationBytes = RavencoinACCTv3.buildQortalAT(tradeNativeAddress, tradeForeignPublicKeyHash, tradeBotCreateRequest.qortAmount,
tradeBotCreateRequest.foreignAmount, tradeBotCreateRequest.tradeTimeout);
long amount = tradeBotCreateRequest.fundingQortAmount;
DeployAtTransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, aTType, tags, creationBytes, amount, Asset.QORT);
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
fee = deployAtTransaction.calcRecommendedFee();
deployAtTransactionData.setFee(fee);
DeployAtTransaction.ensureATAddress(deployAtTransactionData);
String atAddress = deployAtTransactionData.getAtAddress();
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, RavencoinACCTv3.NAME,
State.BOB_WAITING_FOR_AT_CONFIRM.name(), State.BOB_WAITING_FOR_AT_CONFIRM.value,
creator.getAddress(), atAddress, timestamp, tradeBotCreateRequest.qortAmount,
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
null, null,
SupportedBlockchain.RAVENCOIN.name(),
tradeForeignPublicKey, tradeForeignPublicKeyHash,
tradeBotCreateRequest.foreignAmount, null, null, null, ravencoinReceivingAccountInfo);
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Built AT %s. Waiting for deployment", atAddress));
// Attempt to backup the trade bot data
TradeBot.backupTradeBotData(repository, null);
// Return to user for signing and broadcast as we don't have their Qortal private key
try {
return DeployAtTransactionTransformer.toBytes(deployAtTransactionData);
} catch (TransformationException e) {
throw new DataException("Failed to transform DEPLOY_AT transaction?", e);
}
}
/**
* Creates a trade-bot entry from the 'Alice' viewpoint, i.e. matching RVN to an existing offer.
* <p>
* Requires a chosen trade offer from Bob, passed by <tt>crossChainTradeData</tt>
* and access to a Ravencoin wallet via <tt>xprv58</tt>.
* <p>
* The <tt>crossChainTradeData</tt> contains the current trade offer state
* as extracted from the AT's data segment.
* <p>
* Access to a funded wallet is via a Ravencoin BIP32 hierarchical deterministic key,
* passed via <tt>xprv58</tt>.
* <b>This key will be stored in your node's database</b>
* to allow trade-bot to create/fund the necessary P2SH transactions!
* However, due to the nature of BIP32 keys, it is possible to give the trade-bot
* only a subset of wallet access (see BIP32 for more details).
* <p>
* As an example, the xprv58 can be extract from a <i>legacy, password-less</i>
* Electrum wallet by going to the console tab and entering:<br>
* <tt>wallet.keystore.xprv</tt><br>
* which should result in a base58 string starting with either 'xprv' (for Ravencoin main-net)
* or 'tprv' for (Ravencoin test-net).
* <p>
* It is envisaged that the value in <tt>xprv58</tt> will actually come from a Qortal-UI-managed wallet.
* <p>
* If sufficient funds are available, <b>this method will actually fund the P2SH-A</b>
* with the Ravencoin amount expected by 'Bob'.
* <p>
* If the Ravencoin transaction is successfully broadcast to the network then
* we also send a MESSAGE to Bob's trade-bot to let them know.
* <p>
* The trade-bot entry is saved to the repository and the cross-chain trading process commences.
* <p>
* @param repository
* @param crossChainTradeData chosen trade OFFER that Alice wants to match
* @param xprv58 funded wallet xprv in base58
* @return true if P2SH-A funding transaction successfully broadcast to Ravencoin network, false otherwise
* @throws DataException
*/
public ResponseResult startResponse(Repository repository, ATData atData, ACCT acct, CrossChainTradeData crossChainTradeData, String xprv58, String receivingAddress) throws DataException {
byte[] tradePrivateKey = TradeBot.generateTradePrivateKey();
byte[] secretA = TradeBot.generateSecret();
byte[] hashOfSecretA = Crypto.hash160(secretA);
byte[] tradeNativePublicKey = TradeBot.deriveTradeNativePublicKey(tradePrivateKey);
byte[] tradeNativePublicKeyHash = Crypto.hash160(tradeNativePublicKey);
String tradeNativeAddress = Crypto.toAddress(tradeNativePublicKey);
byte[] tradeForeignPublicKey = TradeBot.deriveTradeForeignPublicKey(tradePrivateKey);
byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
byte[] receivingPublicKeyHash = Base58.decode(receivingAddress); // Actually the whole address, not just PKH
// We need to generate lockTime-A: add tradeTimeout to now
long now = NTP.getTime();
int lockTimeA = crossChainTradeData.tradeTimeout * 60 + (int) (now / 1000L);
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, RavencoinACCTv3.NAME,
State.ALICE_WAITING_FOR_AT_LOCK.name(), State.ALICE_WAITING_FOR_AT_LOCK.value,
receivingAddress, crossChainTradeData.qortalAtAddress, now, crossChainTradeData.qortAmount,
tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
secretA, hashOfSecretA,
SupportedBlockchain.RAVENCOIN.name(),
tradeForeignPublicKey, tradeForeignPublicKeyHash,
crossChainTradeData.expectedForeignAmount, xprv58, null, lockTimeA, receivingPublicKeyHash);
// Attempt to backup the trade bot data
// Include tradeBotData as an additional parameter, since it's not in the repository yet
TradeBot.backupTradeBotData(repository, Arrays.asList(tradeBotData));
// Check we have enough funds via xprv58 to fund P2SH to cover expectedForeignAmount
long p2shFee;
try {
p2shFee = Ravencoin.getInstance().getP2shFee(now);
} catch (ForeignBlockchainException e) {
LOGGER.debug("Couldn't estimate Ravencoin fees?");
return ResponseResult.NETWORK_ISSUE;
}
// Fee for redeem/refund is subtracted from P2SH-A balance.
// Do not include fee for funding transaction as this is covered by buildSpend()
long amountA = crossChainTradeData.expectedForeignAmount + p2shFee /*redeeming/refunding P2SH-A*/;
// P2SH-A to be funded
byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(tradeForeignPublicKeyHash, lockTimeA, crossChainTradeData.creatorForeignPKH, hashOfSecretA);
String p2shAddress = Ravencoin.getInstance().deriveP2shAddress(redeemScriptBytes);
// Build transaction for funding P2SH-A
Transaction p2shFundingTransaction = Ravencoin.getInstance().buildSpend(tradeBotData.getForeignKey(), p2shAddress, amountA);
if (p2shFundingTransaction == null) {
LOGGER.debug("Unable to build P2SH-A funding transaction - lack of funds?");
return ResponseResult.BALANCE_ISSUE;
}
try {
Ravencoin.getInstance().broadcastTransaction(p2shFundingTransaction);
} catch (ForeignBlockchainException e) {
// We couldn't fund P2SH-A at this time
LOGGER.debug("Couldn't broadcast P2SH-A funding transaction?");
return ResponseResult.NETWORK_ISSUE;
}
// Attempt to send MESSAGE to Bob's Qortal trade address
byte[] messageData = RavencoinACCTv3.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA());
String messageRecipient = crossChainTradeData.qortalCreatorTradeAddress;
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
if (!isMessageAlreadySent) {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
messageTransaction.computeNonce();
messageTransaction.sign(sender);
// reset repository state to prevent deadlock
repository.discardChanges();
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
return ResponseResult.NETWORK_ISSUE;
}
}
TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress));
return ResponseResult.OK;
}
@Override
public boolean canDelete(Repository repository, TradeBotData tradeBotData) throws DataException {
State tradeBotState = State.valueOf(tradeBotData.getStateValue());
if (tradeBotState == null)
return true;
// If the AT doesn't exist then we might as well let the user tidy up
if (!repository.getATRepository().exists(tradeBotData.getAtAddress()))
return true;
switch (tradeBotState) {
case BOB_WAITING_FOR_AT_CONFIRM:
case ALICE_DONE:
case BOB_DONE:
case ALICE_REFUNDED:
case BOB_REFUNDED:
case ALICE_REFUNDING_A:
return true;
default:
return false;
}
}
@Override
public void progress(Repository repository, TradeBotData tradeBotData) throws DataException, ForeignBlockchainException {
State tradeBotState = State.valueOf(tradeBotData.getStateValue());
if (tradeBotState == null) {
LOGGER.info(() -> String.format("Trade-bot entry for AT %s has invalid state?", tradeBotData.getAtAddress()));
return;
}
ATData atData = null;
CrossChainTradeData tradeData = null;
if (tradeBotState.requiresAtData) {
// Attempt to fetch AT data
atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress());
if (atData == null) {
LOGGER.debug(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress()));
return;
}
if (tradeBotState.requiresTradeData) {
tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
if (tradeData == null) {
LOGGER.warn(() -> String.format("Unable to fetch ACCT trade data for AT %s from repository", tradeBotData.getAtAddress()));
return;
}
}
}
switch (tradeBotState) {
case BOB_WAITING_FOR_AT_CONFIRM:
handleBobWaitingForAtConfirm(repository, tradeBotData);
break;
case BOB_WAITING_FOR_MESSAGE:
TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
handleBobWaitingForMessage(repository, tradeBotData, atData, tradeData);
break;
case ALICE_WAITING_FOR_AT_LOCK:
TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
handleAliceWaitingForAtLock(repository, tradeBotData, atData, tradeData);
break;
case BOB_WAITING_FOR_AT_REDEEM:
TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
handleBobWaitingForAtRedeem(repository, tradeBotData, atData, tradeData);
break;
case ALICE_DONE:
case BOB_DONE:
break;
case ALICE_REFUNDING_A:
TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
handleAliceRefundingP2shA(repository, tradeBotData, atData, tradeData);
break;
case ALICE_REFUNDED:
case BOB_REFUNDED:
break;
}
}
/**
* Trade-bot is waiting for Bob's AT to deploy.
* <p>
* If AT is deployed, then trade-bot's next step is to wait for MESSAGE from Alice.
*/
private void handleBobWaitingForAtConfirm(Repository repository, TradeBotData tradeBotData) throws DataException {
if (!repository.getATRepository().exists(tradeBotData.getAtAddress())) {
if (NTP.getTime() - tradeBotData.getTimestamp() <= MAX_AT_CONFIRMATION_PERIOD)
return;
// We've waited ages for AT to be confirmed into a block but something has gone awry.
// After this long we assume transaction loss so give up with trade-bot entry too.
tradeBotData.setState(State.BOB_REFUNDED.name());
tradeBotData.setStateValue(State.BOB_REFUNDED.value);
tradeBotData.setTimestamp(NTP.getTime());
// We delete trade-bot entry here instead of saving, hence not using updateTradeBotState()
repository.getCrossChainRepository().delete(tradeBotData.getTradePrivateKey());
repository.saveChanges();
LOGGER.info(() -> String.format("AT %s never confirmed. Giving up on trade", tradeBotData.getAtAddress()));
TradeBot.notifyStateChange(tradeBotData);
return;
}
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_WAITING_FOR_MESSAGE,
() -> String.format("AT %s confirmed ready. Waiting for trade message", tradeBotData.getAtAddress()));
}
/**
* Trade-bot is waiting for MESSAGE from Alice's trade-bot, containing Alice's trade info.
* <p>
* It's possible Bob has cancelling his trade offer, receiving an automatic QORT refund,
* in which case trade-bot is done with this specific trade and finalizes on refunded state.
* <p>
* Assuming trade is still on offer, trade-bot checks the contents of MESSAGE from Alice's trade-bot.
* <p>
* Details from Alice are used to derive P2SH-A address and this is checked for funding balance.
* <p>
* Assuming P2SH-A has at least expected Ravencoin balance,
* Bob's trade-bot constructs a zero-fee, PoW MESSAGE to send to Bob's AT with more trade details.
* <p>
* On processing this MESSAGE, Bob's AT should switch into 'TRADE' mode and only trade with Alice.
* <p>
* Trade-bot's next step is to wait for Alice to redeem the AT, which will allow Bob to
* extract secret-A needed to redeem Alice's P2SH.
* @throws ForeignBlockchainException
*/
private void handleBobWaitingForMessage(Repository repository, TradeBotData tradeBotData,
ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
// If AT has finished then Bob likely cancelled his trade offer
if (atData.getIsFinished()) {
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED,
() -> String.format("AT %s cancelled - trading aborted", tradeBotData.getAtAddress()));
return;
}
Ravencoin ravencoin = Ravencoin.getInstance();
String address = tradeBotData.getTradeNativeAddress();
List<MessageTransactionData> messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, address, null, null, null);
for (MessageTransactionData messageTransactionData : messageTransactionsData) {
if (messageTransactionData.isText())
continue;
// We're expecting: HASH160(secret-A), Alice's Ravencoin pubkeyhash and lockTime-A
byte[] messageData = messageTransactionData.getData();
RavencoinACCTv3.OfferMessageData offerMessageData = RavencoinACCTv3.extractOfferMessageData(messageData);
if (offerMessageData == null)
continue;
byte[] aliceForeignPublicKeyHash = offerMessageData.partnerRavencoinPKH;
byte[] hashOfSecretA = offerMessageData.hashOfSecretA;
int lockTimeA = (int) offerMessageData.lockTimeA;
long messageTimestamp = messageTransactionData.getTimestamp();
int refundTimeout = RavencoinACCTv3.calcRefundTimeout(messageTimestamp, lockTimeA);
// Determine P2SH-A address and confirm funded
byte[] redeemScriptA = BitcoinyHTLC.buildScript(aliceForeignPublicKeyHash, lockTimeA, tradeBotData.getTradeForeignPublicKeyHash(), hashOfSecretA);
String p2shAddressA = ravencoin.deriveP2shAddress(redeemScriptA);
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
long p2shFee = Ravencoin.getInstance().getP2shFee(feeTimestamp);
final long minimumAmountA = tradeBotData.getForeignAmount() + p2shFee;
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(ravencoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
switch (htlcStatusA) {
case UNFUNDED:
case FUNDING_IN_PROGRESS:
// There might be another MESSAGE from someone else with an actually funded P2SH-A...
continue;
case REDEEM_IN_PROGRESS:
case REDEEMED:
// We've already redeemed this?
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_DONE,
() -> String.format("P2SH-A %s already spent? Assuming trade complete", p2shAddressA));
return;
case REFUND_IN_PROGRESS:
case REFUNDED:
// This P2SH-A is burnt, but there might be another MESSAGE from someone else with an actually funded P2SH-A...
continue;
case FUNDED:
// Fall-through out of switch...
break;
}
// Good to go - send MESSAGE to AT
String aliceNativeAddress = Crypto.toAddress(messageTransactionData.getCreatorPublicKey());
// Build outgoing message, padding each part to 32 bytes to make it easier for AT to consume
byte[] outgoingMessageData = RavencoinACCTv3.buildTradeMessage(aliceNativeAddress, aliceForeignPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
String messageRecipient = tradeBotData.getAtAddress();
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, outgoingMessageData);
if (!isMessageAlreadySent) {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false);
outgoingMessageTransaction.computeNonce();
outgoingMessageTransaction.sign(sender);
// reset repository state to prevent deadlock
repository.discardChanges();
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
return;
}
}
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_WAITING_FOR_AT_REDEEM,
() -> String.format("Locked AT %s to %s. Waiting for AT redeem", tradeBotData.getAtAddress(), aliceNativeAddress));
return;
}
}
/**
* Trade-bot is waiting for Bob's AT to switch to TRADE mode and lock trade to Alice only.
* <p>
* It's possible that Bob has cancelled his trade offer in the mean time, or that somehow
* this process has taken so long that we've reached P2SH-A's locktime, or that someone else
* has managed to trade with Bob. In any of these cases, trade-bot switches to begin the refunding process.
* <p>
* Assuming Bob's AT is locked to Alice, trade-bot checks AT's state data to make sure it is correct.
* <p>
* If all is well, trade-bot then redeems AT using Alice's secret-A, releasing Bob's QORT to Alice.
* <p>
* In revealing a valid secret-A, Bob can then redeem the RVN funds from P2SH-A.
* <p>
* @throws ForeignBlockchainException
*/
private void handleAliceWaitingForAtLock(Repository repository, TradeBotData tradeBotData,
ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
if (aliceUnexpectedState(repository, tradeBotData, atData, crossChainTradeData))
return;
Ravencoin ravencoin = Ravencoin.getInstance();
int lockTimeA = tradeBotData.getLockTimeA();
// Refund P2SH-A if we've passed lockTime-A
if (NTP.getTime() >= lockTimeA * 1000L) {
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
String p2shAddressA = ravencoin.deriveP2shAddress(redeemScriptA);
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
long p2shFee = Ravencoin.getInstance().getP2shFee(feeTimestamp);
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(ravencoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
switch (htlcStatusA) {
case UNFUNDED:
case FUNDING_IN_PROGRESS:
case FUNDED:
break;
case REDEEM_IN_PROGRESS:
case REDEEMED:
// Already redeemed?
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
() -> String.format("P2SH-A %s already spent? Assuming trade completed", p2shAddressA));
return;
case REFUND_IN_PROGRESS:
case REFUNDED:
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED,
() -> String.format("P2SH-A %s already refunded. Trade aborted", p2shAddressA));
return;
}
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
() -> atData.getIsFinished()
? String.format("AT %s cancelled. Refunding P2SH-A %s - aborting trade", tradeBotData.getAtAddress(), p2shAddressA)
: String.format("LockTime-A reached, refunding P2SH-A %s - aborting trade", p2shAddressA));
return;
}
// We're waiting for AT to be in TRADE mode
if (crossChainTradeData.mode != AcctMode.TRADING)
return;
// AT is in TRADE mode and locked to us as checked by aliceUnexpectedState() above
// Find our MESSAGE to AT from previous state
List<MessageTransactionData> messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(tradeBotData.getTradeNativePublicKey(),
crossChainTradeData.qortalCreatorTradeAddress, null, null, null);
if (messageTransactionsData == null || messageTransactionsData.isEmpty()) {
LOGGER.warn(() -> String.format("Unable to find our message to trade creator %s?", crossChainTradeData.qortalCreatorTradeAddress));
return;
}
long recipientMessageTimestamp = messageTransactionsData.get(0).getTimestamp();
int refundTimeout = RavencoinACCTv3.calcRefundTimeout(recipientMessageTimestamp, lockTimeA);
// Our calculated refundTimeout should match AT's refundTimeout
if (refundTimeout != crossChainTradeData.refundTimeout) {
LOGGER.debug(() -> String.format("Trade AT refundTimeout '%d' doesn't match our refundTimeout '%d'", crossChainTradeData.refundTimeout, refundTimeout));
// We'll eventually refund
return;
}
// We're good to redeem AT
// Send 'redeem' MESSAGE to AT using both secret
byte[] secretA = tradeBotData.getSecret();
String qortalReceivingAddress = Base58.encode(tradeBotData.getReceivingAccountInfo()); // Actually contains whole address, not just PKH
byte[] messageData = RavencoinACCTv3.buildRedeemMessage(secretA, qortalReceivingAddress);
String messageRecipient = tradeBotData.getAtAddress();
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
if (!isMessageAlreadySent) {
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
messageTransaction.computeNonce();
messageTransaction.sign(sender);
// Reset repository state to prevent deadlock
repository.discardChanges();
ValidationResult result = messageTransaction.importAsUnconfirmed();
if (result != ValidationResult.OK) {
LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
return;
}
}
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
() -> String.format("Redeeming AT %s. Funds should arrive at %s",
tradeBotData.getAtAddress(), qortalReceivingAddress));
}
/**
* Trade-bot is waiting for Alice to redeem Bob's AT, thus revealing secret-A which is required to spend the RVN funds from P2SH-A.
* <p>
* It's possible that Bob's AT has reached its trading timeout and automatically refunded QORT back to Bob. In which case,
* trade-bot is done with this specific trade and finalizes in refunded state.
* <p>
* Assuming trade-bot can extract a valid secret-A from Alice's MESSAGE then trade-bot uses that to redeem the RVN funds from P2SH-A
* to Bob's 'foreign'/Ravencoin trade legacy-format address, as derived from trade private key.
* <p>
* (This could potentially be 'improved' to send RVN to any address of Bob's choosing by changing the transaction output).
* <p>
* If trade-bot successfully broadcasts the transaction, then this specific trade is done.
* @throws ForeignBlockchainException
*/
private void handleBobWaitingForAtRedeem(Repository repository, TradeBotData tradeBotData,
ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
// AT should be 'finished' once Alice has redeemed QORT funds
if (!atData.getIsFinished())
// Not finished yet
return;
// If AT is REFUNDED or CANCELLED then something has gone wrong
if (crossChainTradeData.mode == AcctMode.REFUNDED || crossChainTradeData.mode == AcctMode.CANCELLED) {
// Alice hasn't redeemed the QORT, so there is no point in trying to redeem the RVN
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED,
() -> String.format("AT %s has auto-refunded - trade aborted", tradeBotData.getAtAddress()));
return;
}
byte[] secretA = RavencoinACCTv3.getInstance().findSecretA(repository, crossChainTradeData);
if (secretA == null) {
LOGGER.debug(() -> String.format("Unable to find secret-A from redeem message to AT %s?", tradeBotData.getAtAddress()));
return;
}
// Use secret-A to redeem P2SH-A
Ravencoin ravencoin = Ravencoin.getInstance();
byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo();
int lockTimeA = crossChainTradeData.lockTimeA;
byte[] redeemScriptA = BitcoinyHTLC.buildScript(crossChainTradeData.partnerForeignPKH, lockTimeA, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretA);
String p2shAddressA = ravencoin.deriveP2shAddress(redeemScriptA);
// Fee for redeem/refund is subtracted from P2SH-A balance.
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
long p2shFee = Ravencoin.getInstance().getP2shFee(feeTimestamp);
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(ravencoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
switch (htlcStatusA) {
case UNFUNDED:
case FUNDING_IN_PROGRESS:
// P2SH-A suddenly not funded? Our best bet at this point is to hope for AT auto-refund
return;
case REDEEM_IN_PROGRESS:
case REDEEMED:
// Double-check that we have redeemed P2SH-A...
break;
case REFUND_IN_PROGRESS:
case REFUNDED:
// Wait for AT to auto-refund
return;
case FUNDED: {
Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
List<TransactionOutput> fundingOutputs = ravencoin.getUnspentOutputs(p2shAddressA);
Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(ravencoin.getNetworkParameters(), redeemAmount, redeemKey,
fundingOutputs, redeemScriptA, secretA, receivingAccountInfo);
ravencoin.broadcastTransaction(p2shRedeemTransaction);
break;
}
}
String receivingAddress = ravencoin.pkhToAddress(receivingAccountInfo);
TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_DONE,
() -> String.format("P2SH-A %s redeemed. Funds should arrive at %s", tradeBotData.getAtAddress(), receivingAddress));
}
/**
* Trade-bot is attempting to refund P2SH-A.
* @throws ForeignBlockchainException
*/
private void handleAliceRefundingP2shA(Repository repository, TradeBotData tradeBotData,
ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
int lockTimeA = tradeBotData.getLockTimeA();
// We can't refund P2SH-A until lockTime-A has passed
if (NTP.getTime() <= lockTimeA * 1000L)
return;
Ravencoin ravencoin = Ravencoin.getInstance();
// We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
int medianBlockTime = ravencoin.getMedianBlockTime();
if (medianBlockTime <= lockTimeA)
return;
byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
String p2shAddressA = ravencoin.deriveP2shAddress(redeemScriptA);
// Fee for redeem/refund is subtracted from P2SH-A balance.
long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
long p2shFee = Ravencoin.getInstance().getP2shFee(feeTimestamp);
long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(ravencoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
switch (htlcStatusA) {
case UNFUNDED:
case FUNDING_IN_PROGRESS:
// Still waiting for P2SH-A to be funded...
return;
case REDEEM_IN_PROGRESS:
case REDEEMED:
// Too late!
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
() -> String.format("P2SH-A %s already spent!", p2shAddressA));
return;
case REFUND_IN_PROGRESS:
case REFUNDED:
break;
case FUNDED:{
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
List<TransactionOutput> fundingOutputs = ravencoin.getUnspentOutputs(p2shAddressA);
// Determine receive address for refund
String receiveAddress = ravencoin.getUnusedReceiveAddress(tradeBotData.getForeignKey());
Address receiving = Address.fromString(ravencoin.getNetworkParameters(), receiveAddress);
Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(ravencoin.getNetworkParameters(), refundAmount, refundKey,
fundingOutputs, redeemScriptA, lockTimeA, receiving.getHash());
ravencoin.broadcastTransaction(p2shRefundTransaction);
break;
}
}
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED,
() -> String.format("LockTime-A reached. Refunded P2SH-A %s. Trade aborted", p2shAddressA));
}
/**
* Returns true if Alice finds AT unexpectedly cancelled, refunded, redeemed or locked to someone else.
* <p>
* Will automatically update trade-bot state to <tt>ALICE_REFUNDING_A</tt> or <tt>ALICE_DONE</tt> as necessary.
*
* @throws DataException
* @throws ForeignBlockchainException
*/
private boolean aliceUnexpectedState(Repository repository, TradeBotData tradeBotData,
ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
// This is OK
if (!atData.getIsFinished() && crossChainTradeData.mode == AcctMode.OFFERING)
return false;
boolean isAtLockedToUs = tradeBotData.getTradeNativeAddress().equals(crossChainTradeData.qortalPartnerAddress);
if (!atData.getIsFinished() && crossChainTradeData.mode == AcctMode.TRADING)
if (isAtLockedToUs) {
// AT is trading with us - OK
return false;
} else {
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
() -> String.format("AT %s trading with someone else: %s. Refunding & aborting trade", tradeBotData.getAtAddress(), crossChainTradeData.qortalPartnerAddress));
return true;
}
if (atData.getIsFinished() && crossChainTradeData.mode == AcctMode.REDEEMED && isAtLockedToUs) {
// We've redeemed already?
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
() -> String.format("AT %s already redeemed by us. Trade completed", tradeBotData.getAtAddress()));
} else {
// Any other state is not good, so start defensive refund
TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
() -> String.format("AT %s cancelled/refunded/redeemed by someone else/invalid state. Refunding & aborting trade", tradeBotData.getAtAddress()));
}
return true;
}
private long calcFeeTimestamp(int lockTimeA, int tradeTimeout) {
return (lockTimeA - tradeTimeout * 60) * 1000L;
}
}

View File

@@ -100,6 +100,8 @@ public class TradeBot implements Listener {
acctTradeBotSuppliers.put(DogecoinACCTv1.class, DogecoinACCTv1TradeBot::getInstance);
acctTradeBotSuppliers.put(DogecoinACCTv2.class, DogecoinACCTv2TradeBot::getInstance);
acctTradeBotSuppliers.put(DogecoinACCTv3.class, DogecoinACCTv3TradeBot::getInstance);
acctTradeBotSuppliers.put(DigibyteACCTv3.class, DigibyteACCTv3TradeBot::getInstance);
acctTradeBotSuppliers.put(RavencoinACCTv3.class, RavencoinACCTv3TradeBot::getInstance);
}
private static TradeBot instance;

View File

@@ -0,0 +1,171 @@
package org.qortal.crosschain;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumMap;
import java.util.Map;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Context;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.params.RegTestParams;
import org.bitcoinj.params.TestNet3Params;
import org.libdohj.params.DigibyteMainNetParams;
import org.qortal.crosschain.ElectrumX.Server;
import org.qortal.crosschain.ElectrumX.Server.ConnectionType;
import org.qortal.settings.Settings;
public class Digibyte extends Bitcoiny {
public static final String CURRENCY_CODE = "DGB";
private static final Coin DEFAULT_FEE_PER_KB = Coin.valueOf(100000); // 0.001 DGB per 1000 bytes
private static final long MINIMUM_ORDER_AMOUNT = 1000000; // 0.01 DGB minimum order, to avoid dust errors
// Temporary values until a dynamic fee system is written.
private static final long MAINNET_FEE = 10000L;
private static final long NON_MAINNET_FEE = 10000L; // enough for TESTNET3 and should be OK for REGTEST
private static final Map<ConnectionType, Integer> DEFAULT_ELECTRUMX_PORTS = new EnumMap<>(ConnectionType.class);
static {
DEFAULT_ELECTRUMX_PORTS.put(ConnectionType.TCP, 50001);
DEFAULT_ELECTRUMX_PORTS.put(ConnectionType.SSL, 50002);
}
public enum DigibyteNet {
MAIN {
@Override
public NetworkParameters getParams() {
return DigibyteMainNetParams.get();
}
@Override
public Collection<Server> getServers() {
return Arrays.asList(
// Servers chosen on NO BASIS WHATSOEVER from various sources!
// Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=dgb
new Server("electrum1.cipig.net", ConnectionType.SSL, 20059),
new Server("electrum2.cipig.net", ConnectionType.SSL, 20059),
new Server("electrum3.cipig.net", ConnectionType.SSL, 20059));
}
@Override
public String getGenesisHash() {
return "7497ea1b465eb39f1c8f507bc877078fe016d6fcb6dfad3a64c98dcc6e1e8496";
}
@Override
public long getP2shFee(Long timestamp) {
// TODO: This will need to be replaced with something better in the near future!
return MAINNET_FEE;
}
},
TEST3 {
@Override
public NetworkParameters getParams() {
return TestNet3Params.get();
}
@Override
public Collection<Server> getServers() {
return Arrays.asList(); // TODO: find testnet servers
}
@Override
public String getGenesisHash() {
return "308ea0711d5763be2995670dd9ca9872753561285a84da1d58be58acaa822252";
}
@Override
public long getP2shFee(Long timestamp) {
return NON_MAINNET_FEE;
}
},
REGTEST {
@Override
public NetworkParameters getParams() {
return RegTestParams.get();
}
@Override
public Collection<Server> getServers() {
return Arrays.asList(
new Server("localhost", ConnectionType.TCP, 50001),
new Server("localhost", ConnectionType.SSL, 50002));
}
@Override
public String getGenesisHash() {
// This is unique to each regtest instance
return null;
}
@Override
public long getP2shFee(Long timestamp) {
return NON_MAINNET_FEE;
}
};
public abstract NetworkParameters getParams();
public abstract Collection<Server> getServers();
public abstract String getGenesisHash();
public abstract long getP2shFee(Long timestamp) throws ForeignBlockchainException;
}
private static Digibyte instance;
private final DigibyteNet digibyteNet;
// Constructors and instance
private Digibyte(DigibyteNet digibyteNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
super(blockchain, bitcoinjContext, currencyCode);
this.digibyteNet = digibyteNet;
LOGGER.info(() -> String.format("Starting Digibyte support using %s", this.digibyteNet.name()));
}
public static synchronized Digibyte getInstance() {
if (instance == null) {
DigibyteNet digibyteNet = Settings.getInstance().getDigibyteNet();
BitcoinyBlockchainProvider electrumX = new ElectrumX("Digibyte-" + digibyteNet.name(), digibyteNet.getGenesisHash(), digibyteNet.getServers(), DEFAULT_ELECTRUMX_PORTS);
Context bitcoinjContext = new Context(digibyteNet.getParams());
instance = new Digibyte(digibyteNet, electrumX, bitcoinjContext, CURRENCY_CODE);
}
return instance;
}
// Getters & setters
public static synchronized void resetForTesting() {
instance = null;
}
// Actual useful methods for use by other classes
@Override
public Coin getFeePerKb() {
return DEFAULT_FEE_PER_KB;
}
@Override
public long getMinimumOrderAmount() {
return MINIMUM_ORDER_AMOUNT;
}
/**
* Returns estimated DGB fee, in sats per 1000bytes, optionally for historic timestamp.
*
* @param timestamp optional milliseconds since epoch, or null for 'now'
* @return sats per 1000bytes, or throws ForeignBlockchainException if something went wrong
*/
@Override
public long getP2shFee(Long timestamp) throws ForeignBlockchainException {
return this.digibyteNet.getP2shFee(timestamp);
}
}

View File

@@ -0,0 +1,858 @@
package org.qortal.crosschain;
import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.ciyam.at.*;
import org.qortal.account.Account;
import org.qortal.asset.Asset;
import org.qortal.at.QortalFunctionCode;
import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData;
import org.qortal.data.crosschain.CrossChainTradeData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.utils.Base58;
import org.qortal.utils.BitTwiddling;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
import static org.ciyam.at.OpCode.calcOffset;
/**
* Cross-chain trade AT
*
* <p>
* <ul>
* <li>Bob generates Digibyte & Qortal 'trade' keys
* <ul>
* <li>private key required to sign P2SH redeem tx</li>
* <li>private key could be used to create 'secret' (e.g. double-SHA256)</li>
* <li>encrypted private key could be stored in Qortal AT for access by Bob from any node</li>
* </ul>
* </li>
* <li>Bob deploys Qortal AT
* <ul>
* </ul>
* </li>
* <li>Alice finds Qortal AT and wants to trade
* <ul>
* <li>Alice generates Digibyte & Qortal 'trade' keys</li>
* <li>Alice funds Digibyte P2SH-A</li>
* <li>Alice sends 'offer' MESSAGE to Bob from her Qortal trade address, containing:
* <ul>
* <li>hash-of-secret-A</li>
* <li>her 'trade' Digibyte PKH</li>
* </ul>
* </li>
* </ul>
* </li>
* <li>Bob receives "offer" MESSAGE
* <ul>
* <li>Checks Alice's P2SH-A</li>
* <li>Sends 'trade' MESSAGE to Qortal AT from his trade address, containing:
* <ul>
* <li>Alice's trade Qortal address</li>
* <li>Alice's trade Digibyte PKH</li>
* <li>hash-of-secret-A</li>
* </ul>
* </li>
* </ul>
* </li>
* <li>Alice checks Qortal AT to confirm it's locked to her
* <ul>
* <li>Alice sends 'redeem' MESSAGE to Qortal AT from her trade address, containing:
* <ul>
* <li>secret-A</li>
* <li>Qortal receiving address of her chosing</li>
* </ul>
* </li>
* <li>AT's QORT funds are sent to Qortal receiving address</li>
* </ul>
* </li>
* <li>Bob checks AT, extracts secret-A
* <ul>
* <li>Bob redeems P2SH-A using his Digibyte trade key and secret-A</li>
* <li>P2SH-A DGB funds end up at Digibyte address determined by redeem transaction output(s)</li>
* </ul>
* </li>
* </ul>
*/
public class DigibyteACCTv3 implements ACCT {
private static final Logger LOGGER = LogManager.getLogger(DigibyteACCTv3.class);
public static final String NAME = DigibyteACCTv3.class.getSimpleName();
public static final byte[] CODE_BYTES_HASH = HashCode.fromString("e6a7dcd87296fae3ce7d80183bf7660c8e2cb4f8746c6a0421a17148f87a0e1d").asBytes(); // SHA256 of AT code bytes
public static final int SECRET_LENGTH = 32;
/** <b>Value</b> offset into AT segment where 'mode' variable (long) is stored. (Multiply by MachineState.VALUE_SIZE for byte offset). */
private static final int MODE_VALUE_OFFSET = 61;
/** <b>Byte</b> offset into AT state data where 'mode' variable (long) is stored. */
public static final int MODE_BYTE_OFFSET = MachineState.HEADER_LENGTH + (MODE_VALUE_OFFSET * MachineState.VALUE_SIZE);
public static class OfferMessageData {
public byte[] partnerDigibytePKH;
public byte[] hashOfSecretA;
public long lockTimeA;
}
public static final int OFFER_MESSAGE_LENGTH = 20 /*partnerDigibytePKH*/ + 20 /*hashOfSecretA*/ + 8 /*lockTimeA*/;
public static final int TRADE_MESSAGE_LENGTH = 32 /*partner's Qortal trade address (padded from 25 to 32)*/
+ 24 /*partner's Digibyte PKH (padded from 20 to 24)*/
+ 8 /*AT trade timeout (minutes)*/
+ 24 /*hash of secret-A (padded from 20 to 24)*/
+ 8 /*lockTimeA*/;
public static final int REDEEM_MESSAGE_LENGTH = 32 /*secret-A*/ + 32 /*partner's Qortal receiving address padded from 25 to 32*/;
public static final int CANCEL_MESSAGE_LENGTH = 32 /*AT creator's Qortal address*/;
private static DigibyteACCTv3 instance;
private DigibyteACCTv3() {
}
public static synchronized DigibyteACCTv3 getInstance() {
if (instance == null)
instance = new DigibyteACCTv3();
return instance;
}
@Override
public byte[] getCodeBytesHash() {
return CODE_BYTES_HASH;
}
@Override
public int getModeByteOffset() {
return MODE_BYTE_OFFSET;
}
@Override
public ForeignBlockchain getBlockchain() {
return Digibyte.getInstance();
}
/**
* Returns Qortal AT creation bytes for cross-chain trading AT.
* <p>
* <tt>tradeTimeout</tt> (minutes) is the time window for the trade partner to send the
* 32-byte secret to the AT, before the AT automatically refunds the AT's creator.
*
* @param creatorTradeAddress AT creator's trade Qortal address
* @param digibytePublicKeyHash 20-byte HASH160 of creator's trade Digibyte public key
* @param qortAmount how much QORT to pay trade partner if they send correct 32-byte secrets to AT
* @param digibyteAmount how much DGB the AT creator is expecting to trade
* @param tradeTimeout suggested timeout for entire trade
*/
public static byte[] buildQortalAT(String creatorTradeAddress, byte[] digibytePublicKeyHash, long qortAmount, long digibyteAmount, int tradeTimeout) {
if (digibytePublicKeyHash.length != 20)
throw new IllegalArgumentException("Digibyte public key hash should be 20 bytes");
// Labels for data segment addresses
int addrCounter = 0;
// Constants (with corresponding dataByteBuffer.put*() calls below)
final int addrCreatorTradeAddress1 = addrCounter++;
final int addrCreatorTradeAddress2 = addrCounter++;
final int addrCreatorTradeAddress3 = addrCounter++;
final int addrCreatorTradeAddress4 = addrCounter++;
final int addrDigibytePublicKeyHash = addrCounter;
addrCounter += 4;
final int addrQortAmount = addrCounter++;
final int addrDigibyteAmount = addrCounter++;
final int addrTradeTimeout = addrCounter++;
final int addrMessageTxnType = addrCounter++;
final int addrExpectedTradeMessageLength = addrCounter++;
final int addrExpectedRedeemMessageLength = addrCounter++;
final int addrCreatorAddressPointer = addrCounter++;
final int addrQortalPartnerAddressPointer = addrCounter++;
final int addrMessageSenderPointer = addrCounter++;
final int addrTradeMessagePartnerDigibytePKHOffset = addrCounter++;
final int addrPartnerDigibytePKHPointer = addrCounter++;
final int addrTradeMessageHashOfSecretAOffset = addrCounter++;
final int addrHashOfSecretAPointer = addrCounter++;
final int addrRedeemMessageReceivingAddressOffset = addrCounter++;
final int addrMessageDataPointer = addrCounter++;
final int addrMessageDataLength = addrCounter++;
final int addrPartnerReceivingAddressPointer = addrCounter++;
final int addrEndOfConstants = addrCounter;
// Variables
final int addrCreatorAddress1 = addrCounter++;
final int addrCreatorAddress2 = addrCounter++;
final int addrCreatorAddress3 = addrCounter++;
final int addrCreatorAddress4 = addrCounter++;
final int addrQortalPartnerAddress1 = addrCounter++;
final int addrQortalPartnerAddress2 = addrCounter++;
final int addrQortalPartnerAddress3 = addrCounter++;
final int addrQortalPartnerAddress4 = addrCounter++;
final int addrLockTimeA = addrCounter++;
final int addrRefundTimeout = addrCounter++;
final int addrRefundTimestamp = addrCounter++;
final int addrLastTxnTimestamp = addrCounter++;
final int addrBlockTimestamp = addrCounter++;
final int addrTxnType = addrCounter++;
final int addrResult = addrCounter++;
final int addrMessageSender1 = addrCounter++;
final int addrMessageSender2 = addrCounter++;
final int addrMessageSender3 = addrCounter++;
final int addrMessageSender4 = addrCounter++;
final int addrMessageLength = addrCounter++;
final int addrMessageData = addrCounter;
addrCounter += 4;
final int addrHashOfSecretA = addrCounter;
addrCounter += 4;
final int addrPartnerDigibytePKH = addrCounter;
addrCounter += 4;
final int addrPartnerReceivingAddress = addrCounter;
addrCounter += 4;
final int addrMode = addrCounter++;
assert addrMode == MODE_VALUE_OFFSET : String.format("addrMode %d does not match MODE_VALUE_OFFSET %d", addrMode, MODE_VALUE_OFFSET);
// Data segment
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
// AT creator's trade Qortal address, decoded from Base58
assert dataByteBuffer.position() == addrCreatorTradeAddress1 * MachineState.VALUE_SIZE : "addrCreatorTradeAddress1 incorrect";
byte[] creatorTradeAddressBytes = Base58.decode(creatorTradeAddress);
dataByteBuffer.put(Bytes.ensureCapacity(creatorTradeAddressBytes, 32, 0));
// Digibyte public key hash
assert dataByteBuffer.position() == addrDigibytePublicKeyHash * MachineState.VALUE_SIZE : "addrDigibytePublicKeyHash incorrect";
dataByteBuffer.put(Bytes.ensureCapacity(digibytePublicKeyHash, 32, 0));
// Redeem Qort amount
assert dataByteBuffer.position() == addrQortAmount * MachineState.VALUE_SIZE : "addrQortAmount incorrect";
dataByteBuffer.putLong(qortAmount);
// Expected Digibyte amount
assert dataByteBuffer.position() == addrDigibyteAmount * MachineState.VALUE_SIZE : "addrDigibyteAmount incorrect";
dataByteBuffer.putLong(digibyteAmount);
// Suggested trade timeout (minutes)
assert dataByteBuffer.position() == addrTradeTimeout * MachineState.VALUE_SIZE : "addrTradeTimeout incorrect";
dataByteBuffer.putLong(tradeTimeout);
// We're only interested in MESSAGE transactions
assert dataByteBuffer.position() == addrMessageTxnType * MachineState.VALUE_SIZE : "addrMessageTxnType incorrect";
dataByteBuffer.putLong(API.ATTransactionType.MESSAGE.value);
// Expected length of 'trade' MESSAGE data from AT creator
assert dataByteBuffer.position() == addrExpectedTradeMessageLength * MachineState.VALUE_SIZE : "addrExpectedTradeMessageLength incorrect";
dataByteBuffer.putLong(TRADE_MESSAGE_LENGTH);
// Expected length of 'redeem' MESSAGE data from trade partner
assert dataByteBuffer.position() == addrExpectedRedeemMessageLength * MachineState.VALUE_SIZE : "addrExpectedRedeemMessageLength incorrect";
dataByteBuffer.putLong(REDEEM_MESSAGE_LENGTH);
// Index into data segment of AT creator's address, used by GET_B_IND
assert dataByteBuffer.position() == addrCreatorAddressPointer * MachineState.VALUE_SIZE : "addrCreatorAddressPointer incorrect";
dataByteBuffer.putLong(addrCreatorAddress1);
// Index into data segment of partner's Qortal address, used by SET_B_IND
assert dataByteBuffer.position() == addrQortalPartnerAddressPointer * MachineState.VALUE_SIZE : "addrQortalPartnerAddressPointer incorrect";
dataByteBuffer.putLong(addrQortalPartnerAddress1);
// Index into data segment of (temporary) transaction's sender's address, used by GET_B_IND
assert dataByteBuffer.position() == addrMessageSenderPointer * MachineState.VALUE_SIZE : "addrMessageSenderPointer incorrect";
dataByteBuffer.putLong(addrMessageSender1);
// Offset into 'trade' MESSAGE data payload for extracting partner's Digibyte PKH
assert dataByteBuffer.position() == addrTradeMessagePartnerDigibytePKHOffset * MachineState.VALUE_SIZE : "addrTradeMessagePartnerDigibytePKHOffset incorrect";
dataByteBuffer.putLong(32L);
// Index into data segment of partner's Digibyte PKH, used by GET_B_IND
assert dataByteBuffer.position() == addrPartnerDigibytePKHPointer * MachineState.VALUE_SIZE : "addrPartnerDigibytePKHPointer incorrect";
dataByteBuffer.putLong(addrPartnerDigibytePKH);
// Offset into 'trade' MESSAGE data payload for extracting hash-of-secret-A
assert dataByteBuffer.position() == addrTradeMessageHashOfSecretAOffset * MachineState.VALUE_SIZE : "addrTradeMessageHashOfSecretAOffset incorrect";
dataByteBuffer.putLong(64L);
// Index into data segment to hash of secret A, used by GET_B_IND
assert dataByteBuffer.position() == addrHashOfSecretAPointer * MachineState.VALUE_SIZE : "addrHashOfSecretAPointer incorrect";
dataByteBuffer.putLong(addrHashOfSecretA);
// Offset into 'redeem' MESSAGE data payload for extracting Qortal receiving address
assert dataByteBuffer.position() == addrRedeemMessageReceivingAddressOffset * MachineState.VALUE_SIZE : "addrRedeemMessageReceivingAddressOffset incorrect";
dataByteBuffer.putLong(32L);
// Source location and length for hashing any passed secret
assert dataByteBuffer.position() == addrMessageDataPointer * MachineState.VALUE_SIZE : "addrMessageDataPointer incorrect";
dataByteBuffer.putLong(addrMessageData);
assert dataByteBuffer.position() == addrMessageDataLength * MachineState.VALUE_SIZE : "addrMessageDataLength incorrect";
dataByteBuffer.putLong(32L);
// Pointer into data segment of where to save partner's receiving Qortal address, used by GET_B_IND
assert dataByteBuffer.position() == addrPartnerReceivingAddressPointer * MachineState.VALUE_SIZE : "addrPartnerReceivingAddressPointer incorrect";
dataByteBuffer.putLong(addrPartnerReceivingAddress);
assert dataByteBuffer.position() == addrEndOfConstants * MachineState.VALUE_SIZE : "dataByteBuffer position not at end of constants";
// Code labels
Integer labelRefund = null;
Integer labelTradeTxnLoop = null;
Integer labelCheckTradeTxn = null;
Integer labelCheckCancelTxn = null;
Integer labelNotTradeNorCancelTxn = null;
Integer labelCheckNonRefundTradeTxn = null;
Integer labelTradeTxnExtract = null;
Integer labelRedeemTxnLoop = null;
Integer labelCheckRedeemTxn = null;
Integer labelCheckRedeemTxnSender = null;
Integer labelPayout = null;
ByteBuffer codeByteBuffer = ByteBuffer.allocate(768);
// Two-pass version
for (int pass = 0; pass < 2; ++pass) {
codeByteBuffer.clear();
try {
/* Initialization */
// Use AT creation 'timestamp' as starting point for finding transactions sent to AT
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrLastTxnTimestamp));
/* NOP - to ensure DIGIBYTE ACCT is unique */
codeByteBuffer.put(OpCode.NOP.compile());
// Load B register with AT creator's address so we can save it into addrCreatorAddress1-4
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_CREATOR_INTO_B));
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrCreatorAddressPointer));
// Set restart position to after this opcode
codeByteBuffer.put(OpCode.SET_PCS.compile());
/* Loop, waiting for message from AT creator's trade address containing trade partner details, or AT owner's address to cancel offer */
/* Transaction processing loop */
labelTradeTxnLoop = codeByteBuffer.position();
/* Sleep until message arrives */
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.SLEEP_UNTIL_MESSAGE.value, addrLastTxnTimestamp));
// Find next transaction (if any) to this AT since the last one (referenced by addrLastTxnTimestamp)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxnTimestamp));
// If no transaction found, A will be zero. If A is zero, set addrResult to 1, otherwise 0.
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.CHECK_A_IS_ZERO, addrResult));
// If addrResult is zero (i.e. A is non-zero, transaction was found) then go check transaction
codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelCheckTradeTxn)));
// Stop and wait for next block
codeByteBuffer.put(OpCode.STP_IMD.compile());
/* Check transaction */
labelCheckTradeTxn = codeByteBuffer.position();
// Update our 'last found transaction's timestamp' using 'timestamp' from transaction
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A, addrLastTxnTimestamp));
// Extract transaction type (message/payment) from transaction and save type in addrTxnType
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TYPE_FROM_TX_IN_A, addrTxnType));
// If transaction type is not MESSAGE type then go look for another transaction
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxnType, addrMessageTxnType, calcOffset(codeByteBuffer, labelTradeTxnLoop)));
/* Check transaction's sender. We're expecting AT creator's trade address for 'trade' message, or AT creator's own address for 'cancel' message. */
// Extract sender address from transaction into B register
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_ADDRESS_FROM_TX_IN_A_INTO_B));
// Save B register into data segment starting at addrMessageSender1 (as pointed to by addrMessageSenderPointer)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageSenderPointer));
// Compare each part of message sender's address with AT creator's trade address. If they don't match, check for cancel situation.
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrCreatorTradeAddress1, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrCreatorTradeAddress2, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrCreatorTradeAddress3, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrCreatorTradeAddress4, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
// Message sender's address matches AT creator's trade address so go process 'trade' message
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelCheckNonRefundTradeTxn == null ? 0 : labelCheckNonRefundTradeTxn));
/* Checking message sender for possible cancel message */
labelCheckCancelTxn = codeByteBuffer.position();
// Compare each part of message sender's address with AT creator's address. If they don't match, look for another transaction.
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrCreatorAddress1, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrCreatorAddress2, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrCreatorAddress3, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrCreatorAddress4, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
// Partner address is AT creator's address, so cancel offer and finish.
codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.CANCELLED.value));
// We're finished forever (finishing auto-refunds remaining balance to AT creator)
codeByteBuffer.put(OpCode.FIN_IMD.compile());
/* Not trade nor cancel message */
labelNotTradeNorCancelTxn = codeByteBuffer.position();
// Loop to find another transaction
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelTradeTxnLoop == null ? 0 : labelTradeTxnLoop));
/* Possible switch-to-trade-mode message */
labelCheckNonRefundTradeTxn = codeByteBuffer.position();
// Check 'trade' message we received has expected number of message bytes
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(QortalFunctionCode.GET_MESSAGE_LENGTH_FROM_TX_IN_A.value, addrMessageLength));
// If message length matches, branch to info extraction code
codeByteBuffer.put(OpCode.BEQ_DAT.compile(addrMessageLength, addrExpectedTradeMessageLength, calcOffset(codeByteBuffer, labelTradeTxnExtract)));
// Message length didn't match - go back to finding another 'trade' MESSAGE transaction
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelTradeTxnLoop == null ? 0 : labelTradeTxnLoop));
/* Extracting info from 'trade' MESSAGE transaction */
labelTradeTxnExtract = codeByteBuffer.position();
// Extract message from transaction into B register
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B));
// Save B register into data segment starting at addrQortalPartnerAddress1 (as pointed to by addrQortalPartnerAddressPointer)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrQortalPartnerAddressPointer));
// Extract trade partner's Digibyte public key hash (PKH) from message into B
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessagePartnerDigibytePKHOffset));
// Store partner's Digibyte PKH (we only really use values from B1-B3)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerDigibytePKHPointer));
// Extract AT trade timeout (minutes) (from B4)
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B4, addrRefundTimeout));
// Grab next 32 bytes
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessageHashOfSecretAOffset));
// Extract hash-of-secret-A (we only really use values from B1-B3)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrHashOfSecretAPointer));
// Extract lockTime-A (from B4)
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B4, addrLockTimeA));
// Calculate trade timeout refund 'timestamp' by adding addrRefundTimeout minutes to this transaction's 'timestamp', then save into addrRefundTimestamp
codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.ADD_MINUTES_TO_TIMESTAMP, addrRefundTimestamp, addrLastTxnTimestamp, addrRefundTimeout));
/* We are in 'trade mode' */
codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.TRADING.value));
// Set restart position to after this opcode
codeByteBuffer.put(OpCode.SET_PCS.compile());
/* Loop, waiting for trade timeout or 'redeem' MESSAGE from Qortal trade partner */
// Fetch current block 'timestamp'
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_BLOCK_TIMESTAMP, addrBlockTimestamp));
// If we're not past refund 'timestamp' then look for next transaction
codeByteBuffer.put(OpCode.BLT_DAT.compile(addrBlockTimestamp, addrRefundTimestamp, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
// We're past refund 'timestamp' so go refund everything back to AT creator
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRefund == null ? 0 : labelRefund));
/* Transaction processing loop */
labelRedeemTxnLoop = codeByteBuffer.position();
// Find next transaction to this AT since the last one (if any)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxnTimestamp));
// If no transaction found, A will be zero. If A is zero, set addrComparator to 1, otherwise 0.
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.CHECK_A_IS_ZERO, addrResult));
// If addrResult is zero (i.e. A is non-zero, transaction was found) then go check transaction
codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelCheckRedeemTxn)));
// Stop and wait for next block
codeByteBuffer.put(OpCode.STP_IMD.compile());
/* Check transaction */
labelCheckRedeemTxn = codeByteBuffer.position();
// Update our 'last found transaction's timestamp' using 'timestamp' from transaction
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A, addrLastTxnTimestamp));
// Extract transaction type (message/payment) from transaction and save type in addrTxnType
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TYPE_FROM_TX_IN_A, addrTxnType));
// If transaction type is not MESSAGE type then go look for another transaction
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxnType, addrMessageTxnType, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
/* Check message payload length */
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(QortalFunctionCode.GET_MESSAGE_LENGTH_FROM_TX_IN_A.value, addrMessageLength));
// If message length matches, branch to sender checking code
codeByteBuffer.put(OpCode.BEQ_DAT.compile(addrMessageLength, addrExpectedRedeemMessageLength, calcOffset(codeByteBuffer, labelCheckRedeemTxnSender)));
// Message length didn't match - go back to finding another 'redeem' MESSAGE transaction
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRedeemTxnLoop == null ? 0 : labelRedeemTxnLoop));
/* Check transaction's sender */
labelCheckRedeemTxnSender = codeByteBuffer.position();
// Extract sender address from transaction into B register
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_ADDRESS_FROM_TX_IN_A_INTO_B));
// Save B register into data segment starting at addrMessageSender1 (as pointed to by addrMessageSenderPointer)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageSenderPointer));
// Compare each part of transaction's sender's address with expected address. If they don't match, look for another transaction.
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrQortalPartnerAddress1, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrQortalPartnerAddress2, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrQortalPartnerAddress3, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrQortalPartnerAddress4, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
/* Check 'secret-A' in transaction's message */
// Extract secret-A from first 32 bytes of message from transaction into B register
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B));
// Save B register into data segment starting at addrMessageData (as pointed to by addrMessageDataPointer)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageDataPointer));
// Load B register with expected hash result (as pointed to by addrHashOfSecretAPointer)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrHashOfSecretAPointer));
// Perform HASH160 using source data at addrMessageData. (Location and length specified via addrMessageDataPointer and addrMessageDataLength).
// Save the equality result (1 if they match, 0 otherwise) into addrResult.
codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.CHECK_HASH160_WITH_B, addrResult, addrMessageDataPointer, addrMessageDataLength));
// If hashes don't match, addrResult will be zero so go find another transaction
codeByteBuffer.put(OpCode.BNZ_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelPayout)));
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRedeemTxnLoop == null ? 0 : labelRedeemTxnLoop));
/* Success! Pay arranged amount to receiving address */
labelPayout = codeByteBuffer.position();
// Extract Qortal receiving address from next 32 bytes of message from transaction into B register
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrRedeemMessageReceivingAddressOffset));
// Save B register into data segment starting at addrPartnerReceivingAddress (as pointed to by addrPartnerReceivingAddressPointer)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerReceivingAddressPointer));
// Pay AT's balance to receiving address
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PAY_TO_ADDRESS_IN_B, addrQortAmount));
// Set redeemed mode
codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.REDEEMED.value));
// We're finished forever (finishing auto-refunds remaining balance to AT creator)
codeByteBuffer.put(OpCode.FIN_IMD.compile());
// Fall-through to refunding any remaining balance back to AT creator
/* Refund balance back to AT creator */
labelRefund = codeByteBuffer.position();
// Set refunded mode
codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.REFUNDED.value));
// We're finished forever (finishing auto-refunds remaining balance to AT creator)
codeByteBuffer.put(OpCode.FIN_IMD.compile());
} catch (CompilationException e) {
throw new IllegalStateException("Unable to compile DGB-QORT ACCT?", e);
}
}
codeByteBuffer.flip();
byte[] codeBytes = new byte[codeByteBuffer.limit()];
codeByteBuffer.get(codeBytes);
assert Arrays.equals(Crypto.digest(codeBytes), DigibyteACCTv3.CODE_BYTES_HASH)
: String.format("BTCACCT.CODE_BYTES_HASH mismatch: expected %s, actual %s", HashCode.fromBytes(CODE_BYTES_HASH), HashCode.fromBytes(Crypto.digest(codeBytes)));
final short ciyamAtVersion = 2;
final short numCallStackPages = 0;
final short numUserStackPages = 0;
final long minActivationAmount = 0L;
return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount);
}
/**
* Returns CrossChainTradeData with useful info extracted from AT.
*/
@Override
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
}
/**
* Returns CrossChainTradeData with useful info extracted from AT.
*/
@Override
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
}
/**
* Returns CrossChainTradeData with useful info extracted from AT.
*/
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData) throws DataException {
byte[] addressBytes = new byte[25]; // for general use
String atAddress = atStateData.getATAddress();
CrossChainTradeData tradeData = new CrossChainTradeData();
tradeData.foreignBlockchain = SupportedBlockchain.DIGIBYTE.name();
tradeData.acctName = NAME;
tradeData.qortalAtAddress = atAddress;
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
tradeData.creationTimestamp = creationTimestamp;
Account atAccount = new Account(repository, atAddress);
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
byte[] stateData = atStateData.getStateData();
ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData);
dataByteBuffer.position(MachineState.HEADER_LENGTH);
/* Constants */
// Skip creator's trade address
dataByteBuffer.get(addressBytes);
tradeData.qortalCreatorTradeAddress = Base58.encode(addressBytes);
dataByteBuffer.position(dataByteBuffer.position() + 32 - addressBytes.length);
// Creator's Digibyte/foreign public key hash
tradeData.creatorForeignPKH = new byte[20];
dataByteBuffer.get(tradeData.creatorForeignPKH);
dataByteBuffer.position(dataByteBuffer.position() + 32 - tradeData.creatorForeignPKH.length); // skip to 32 bytes
// We don't use secret-B
tradeData.hashOfSecretB = null;
// Redeem payout
tradeData.qortAmount = dataByteBuffer.getLong();
// Expected DGB amount
tradeData.expectedForeignAmount = dataByteBuffer.getLong();
// Trade timeout
tradeData.tradeTimeout = (int) dataByteBuffer.getLong();
// Skip MESSAGE transaction type
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip expected 'trade' message length
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip expected 'redeem' message length
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip pointer to creator's address
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip pointer to partner's Qortal trade address
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip pointer to message sender
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip 'trade' message data offset for partner's Digibyte PKH
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip pointer to partner's Digibyte PKH
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip 'trade' message data offset for hash-of-secret-A
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip pointer to hash-of-secret-A
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip 'redeem' message data offset for partner's Qortal receiving address
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip pointer to message data
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip message data length
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip pointer to partner's receiving address
dataByteBuffer.position(dataByteBuffer.position() + 8);
/* End of constants / begin variables */
// Skip AT creator's address
dataByteBuffer.position(dataByteBuffer.position() + 8 * 4);
// Partner's trade address (if present)
dataByteBuffer.get(addressBytes);
String qortalRecipient = Base58.encode(addressBytes);
dataByteBuffer.position(dataByteBuffer.position() + 32 - addressBytes.length);
// Potential lockTimeA (if in trade mode)
int lockTimeA = (int) dataByteBuffer.getLong();
// AT refund timeout (probably only useful for debugging)
int refundTimeout = (int) dataByteBuffer.getLong();
// Trade-mode refund timestamp (AT 'timestamp' converted to Qortal block height)
long tradeRefundTimestamp = dataByteBuffer.getLong();
// Skip last transaction timestamp
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip block timestamp
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip transaction type
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip temporary result
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip temporary message sender
dataByteBuffer.position(dataByteBuffer.position() + 8 * 4);
// Skip message length
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip temporary message data
dataByteBuffer.position(dataByteBuffer.position() + 8 * 4);
// Potential hash160 of secret A
byte[] hashOfSecretA = new byte[20];
dataByteBuffer.get(hashOfSecretA);
dataByteBuffer.position(dataByteBuffer.position() + 32 - hashOfSecretA.length); // skip to 32 bytes
// Potential partner's Digibyte PKH
byte[] partnerDigibytePKH = new byte[20];
dataByteBuffer.get(partnerDigibytePKH);
dataByteBuffer.position(dataByteBuffer.position() + 32 - partnerDigibytePKH.length); // skip to 32 bytes
// Partner's receiving address (if present)
byte[] partnerReceivingAddress = new byte[25];
dataByteBuffer.get(partnerReceivingAddress);
dataByteBuffer.position(dataByteBuffer.position() + 32 - partnerReceivingAddress.length); // skip to 32 bytes
// Trade AT's 'mode'
long modeValue = dataByteBuffer.getLong();
AcctMode mode = AcctMode.valueOf((int) (modeValue & 0xffL));
/* End of variables */
if (mode != null && mode != AcctMode.OFFERING) {
tradeData.mode = mode;
tradeData.refundTimeout = refundTimeout;
tradeData.tradeRefundHeight = new Timestamp(tradeRefundTimestamp).blockHeight;
tradeData.qortalPartnerAddress = qortalRecipient;
tradeData.hashOfSecretA = hashOfSecretA;
tradeData.partnerForeignPKH = partnerDigibytePKH;
tradeData.lockTimeA = lockTimeA;
if (mode == AcctMode.REDEEMED)
tradeData.qortalPartnerReceivingAddress = Base58.encode(partnerReceivingAddress);
} else {
tradeData.mode = AcctMode.OFFERING;
}
tradeData.duplicateDeprecated();
return tradeData;
}
/** Returns 'offer' MESSAGE payload for trade partner to send to AT creator's trade address. */
public static byte[] buildOfferMessage(byte[] partnerBitcoinPKH, byte[] hashOfSecretA, int lockTimeA) {
byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA);
return Bytes.concat(partnerBitcoinPKH, hashOfSecretA, lockTimeABytes);
}
/** Returns info extracted from 'offer' MESSAGE payload sent by trade partner to AT creator's trade address, or null if not valid. */
public static OfferMessageData extractOfferMessageData(byte[] messageData) {
if (messageData == null || messageData.length != OFFER_MESSAGE_LENGTH)
return null;
OfferMessageData offerMessageData = new OfferMessageData();
offerMessageData.partnerDigibytePKH = Arrays.copyOfRange(messageData, 0, 20);
offerMessageData.hashOfSecretA = Arrays.copyOfRange(messageData, 20, 40);
offerMessageData.lockTimeA = BitTwiddling.longFromBEBytes(messageData, 40);
return offerMessageData;
}
/** Returns 'trade' MESSAGE payload for AT creator to send to AT. */
public static byte[] buildTradeMessage(String partnerQortalTradeAddress, byte[] partnerBitcoinPKH, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
byte[] data = new byte[TRADE_MESSAGE_LENGTH];
byte[] partnerQortalAddressBytes = Base58.decode(partnerQortalTradeAddress);
byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA);
byte[] refundTimeoutBytes = BitTwiddling.toBEByteArray((long) refundTimeout);
System.arraycopy(partnerQortalAddressBytes, 0, data, 0, partnerQortalAddressBytes.length);
System.arraycopy(partnerBitcoinPKH, 0, data, 32, partnerBitcoinPKH.length);
System.arraycopy(refundTimeoutBytes, 0, data, 56, refundTimeoutBytes.length);
System.arraycopy(hashOfSecretA, 0, data, 64, hashOfSecretA.length);
System.arraycopy(lockTimeABytes, 0, data, 88, lockTimeABytes.length);
return data;
}
/** Returns 'cancel' MESSAGE payload for AT creator to cancel trade AT. */
@Override
public byte[] buildCancelMessage(String creatorQortalAddress) {
byte[] data = new byte[CANCEL_MESSAGE_LENGTH];
byte[] creatorQortalAddressBytes = Base58.decode(creatorQortalAddress);
System.arraycopy(creatorQortalAddressBytes, 0, data, 0, creatorQortalAddressBytes.length);
return data;
}
/** Returns 'redeem' MESSAGE payload for trade partner to send to AT. */
public static byte[] buildRedeemMessage(byte[] secretA, String qortalReceivingAddress) {
byte[] data = new byte[REDEEM_MESSAGE_LENGTH];
byte[] qortalReceivingAddressBytes = Base58.decode(qortalReceivingAddress);
System.arraycopy(secretA, 0, data, 0, secretA.length);
System.arraycopy(qortalReceivingAddressBytes, 0, data, 32, qortalReceivingAddressBytes.length);
return data;
}
/** Returns refund timeout (minutes) based on trade partner's 'offer' MESSAGE timestamp and P2SH-A locktime. */
public static int calcRefundTimeout(long offerMessageTimestamp, int lockTimeA) {
// refund should be triggered halfway between offerMessageTimestamp and lockTimeA
return (int) ((lockTimeA - (offerMessageTimestamp / 1000L)) / 2L / 60L);
}
@Override
public byte[] findSecretA(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException {
String atAddress = crossChainTradeData.qortalAtAddress;
String redeemerAddress = crossChainTradeData.qortalPartnerAddress;
// We don't have partner's public key so we check every message to AT
List<MessageTransactionData> messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, atAddress, null, null, null);
if (messageTransactionsData == null)
return null;
// Find 'redeem' message
for (MessageTransactionData messageTransactionData : messageTransactionsData) {
// Check message payload type/encryption
if (messageTransactionData.isText() || messageTransactionData.isEncrypted())
continue;
// Check message payload size
byte[] messageData = messageTransactionData.getData();
if (messageData.length != REDEEM_MESSAGE_LENGTH)
// Wrong payload length
continue;
// Check sender
if (!Crypto.toAddress(messageTransactionData.getSenderPublicKey()).equals(redeemerAddress))
// Wrong sender;
continue;
// Extract secretA
byte[] secretA = new byte[32];
System.arraycopy(messageData, 0, secretA, 0, secretA.length);
byte[] hashOfSecretA = Crypto.hash160(secretA);
if (!Arrays.equals(hashOfSecretA, crossChainTradeData.hashOfSecretA))
continue;
return secretA;
}
return null;
}
}

View File

@@ -0,0 +1,175 @@
package org.qortal.crosschain;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumMap;
import java.util.Map;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Context;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.params.RegTestParams;
import org.bitcoinj.params.TestNet3Params;
import org.libdohj.params.RavencoinMainNetParams;
import org.qortal.crosschain.ElectrumX.Server;
import org.qortal.crosschain.ElectrumX.Server.ConnectionType;
import org.qortal.settings.Settings;
public class Ravencoin extends Bitcoiny {
public static final String CURRENCY_CODE = "RVN";
private static final Coin DEFAULT_FEE_PER_KB = Coin.valueOf(1125000); // 0.01125 RVN per 1000 bytes
private static final long MINIMUM_ORDER_AMOUNT = 1000000; // 0.01 RVN minimum order, to avoid dust errors
// Temporary values until a dynamic fee system is written.
private static final long MAINNET_FEE = 1000000L;
private static final long NON_MAINNET_FEE = 1000000L; // enough for TESTNET3 and should be OK for REGTEST
private static final Map<ConnectionType, Integer> DEFAULT_ELECTRUMX_PORTS = new EnumMap<>(ConnectionType.class);
static {
DEFAULT_ELECTRUMX_PORTS.put(ConnectionType.TCP, 50001);
DEFAULT_ELECTRUMX_PORTS.put(ConnectionType.SSL, 50002);
}
public enum RavencoinNet {
MAIN {
@Override
public NetworkParameters getParams() {
return RavencoinMainNetParams.get();
}
@Override
public Collection<Server> getServers() {
return Arrays.asList(
// Servers chosen on NO BASIS WHATSOEVER from various sources!
// Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=rvn
new Server("aethyn.com", ConnectionType.SSL, 50002),
new Server("electrum2.rvn.rocks", ConnectionType.SSL, 50002),
new Server("rvn-dashboard.com", ConnectionType.SSL, 50002),
new Server("rvn4lyfe.com", ConnectionType.SSL, 50002),
new Server("electrum1.cipig.net", ConnectionType.SSL, 20051),
new Server("electrum2.cipig.net", ConnectionType.SSL, 20051),
new Server("electrum3.cipig.net", ConnectionType.SSL, 20051));
}
@Override
public String getGenesisHash() {
return "0000006b444bc2f2ffe627be9d9e7e7a0730000870ef6eb6da46c8eae389df90";
}
@Override
public long getP2shFee(Long timestamp) {
// TODO: This will need to be replaced with something better in the near future!
return MAINNET_FEE;
}
},
TEST3 {
@Override
public NetworkParameters getParams() {
return TestNet3Params.get();
}
@Override
public Collection<Server> getServers() {
return Arrays.asList(); // TODO: find testnet servers
}
@Override
public String getGenesisHash() {
return "000000ecfc5e6324a079542221d00e10362bdc894d56500c414060eea8a3ad5a";
}
@Override
public long getP2shFee(Long timestamp) {
return NON_MAINNET_FEE;
}
},
REGTEST {
@Override
public NetworkParameters getParams() {
return RegTestParams.get();
}
@Override
public Collection<Server> getServers() {
return Arrays.asList(
new Server("localhost", ConnectionType.TCP, 50001),
new Server("localhost", ConnectionType.SSL, 50002));
}
@Override
public String getGenesisHash() {
// This is unique to each regtest instance
return null;
}
@Override
public long getP2shFee(Long timestamp) {
return NON_MAINNET_FEE;
}
};
public abstract NetworkParameters getParams();
public abstract Collection<Server> getServers();
public abstract String getGenesisHash();
public abstract long getP2shFee(Long timestamp) throws ForeignBlockchainException;
}
private static Ravencoin instance;
private final RavencoinNet ravencoinNet;
// Constructors and instance
private Ravencoin(RavencoinNet ravencoinNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
super(blockchain, bitcoinjContext, currencyCode);
this.ravencoinNet = ravencoinNet;
LOGGER.info(() -> String.format("Starting Ravencoin support using %s", this.ravencoinNet.name()));
}
public static synchronized Ravencoin getInstance() {
if (instance == null) {
RavencoinNet ravencoinNet = Settings.getInstance().getRavencoinNet();
BitcoinyBlockchainProvider electrumX = new ElectrumX("Ravencoin-" + ravencoinNet.name(), ravencoinNet.getGenesisHash(), ravencoinNet.getServers(), DEFAULT_ELECTRUMX_PORTS);
Context bitcoinjContext = new Context(ravencoinNet.getParams());
instance = new Ravencoin(ravencoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
}
return instance;
}
// Getters & setters
public static synchronized void resetForTesting() {
instance = null;
}
// Actual useful methods for use by other classes
@Override
public Coin getFeePerKb() {
return DEFAULT_FEE_PER_KB;
}
@Override
public long getMinimumOrderAmount() {
return MINIMUM_ORDER_AMOUNT;
}
/**
* Returns estimated RVN fee, in sats per 1000bytes, optionally for historic timestamp.
*
* @param timestamp optional milliseconds since epoch, or null for 'now'
* @return sats per 1000bytes, or throws ForeignBlockchainException if something went wrong
*/
@Override
public long getP2shFee(Long timestamp) throws ForeignBlockchainException {
return this.ravencoinNet.getP2shFee(timestamp);
}
}

View File

@@ -0,0 +1,858 @@
package org.qortal.crosschain;
import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.ciyam.at.*;
import org.qortal.account.Account;
import org.qortal.asset.Asset;
import org.qortal.at.QortalFunctionCode;
import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData;
import org.qortal.data.crosschain.CrossChainTradeData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.utils.Base58;
import org.qortal.utils.BitTwiddling;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
import static org.ciyam.at.OpCode.calcOffset;
/**
* Cross-chain trade AT
*
* <p>
* <ul>
* <li>Bob generates Ravencoin & Qortal 'trade' keys
* <ul>
* <li>private key required to sign P2SH redeem tx</li>
* <li>private key could be used to create 'secret' (e.g. double-SHA256)</li>
* <li>encrypted private key could be stored in Qortal AT for access by Bob from any node</li>
* </ul>
* </li>
* <li>Bob deploys Qortal AT
* <ul>
* </ul>
* </li>
* <li>Alice finds Qortal AT and wants to trade
* <ul>
* <li>Alice generates Ravencoin & Qortal 'trade' keys</li>
* <li>Alice funds Ravencoin P2SH-A</li>
* <li>Alice sends 'offer' MESSAGE to Bob from her Qortal trade address, containing:
* <ul>
* <li>hash-of-secret-A</li>
* <li>her 'trade' Ravencoin PKH</li>
* </ul>
* </li>
* </ul>
* </li>
* <li>Bob receives "offer" MESSAGE
* <ul>
* <li>Checks Alice's P2SH-A</li>
* <li>Sends 'trade' MESSAGE to Qortal AT from his trade address, containing:
* <ul>
* <li>Alice's trade Qortal address</li>
* <li>Alice's trade Ravencoin PKH</li>
* <li>hash-of-secret-A</li>
* </ul>
* </li>
* </ul>
* </li>
* <li>Alice checks Qortal AT to confirm it's locked to her
* <ul>
* <li>Alice sends 'redeem' MESSAGE to Qortal AT from her trade address, containing:
* <ul>
* <li>secret-A</li>
* <li>Qortal receiving address of her chosing</li>
* </ul>
* </li>
* <li>AT's QORT funds are sent to Qortal receiving address</li>
* </ul>
* </li>
* <li>Bob checks AT, extracts secret-A
* <ul>
* <li>Bob redeems P2SH-A using his Ravencoin trade key and secret-A</li>
* <li>P2SH-A RVN funds end up at Ravencoin address determined by redeem transaction output(s)</li>
* </ul>
* </li>
* </ul>
*/
public class RavencoinACCTv3 implements ACCT {
private static final Logger LOGGER = LogManager.getLogger(RavencoinACCTv3.class);
public static final String NAME = RavencoinACCTv3.class.getSimpleName();
public static final byte[] CODE_BYTES_HASH = HashCode.fromString("91395fa1ec0dfa35beddb0a7f4cc0a1bede157c38787ddb0af0cf03dfdc10f77").asBytes(); // SHA256 of AT code bytes
public static final int SECRET_LENGTH = 32;
/** <b>Value</b> offset into AT segment where 'mode' variable (long) is stored. (Multiply by MachineState.VALUE_SIZE for byte offset). */
private static final int MODE_VALUE_OFFSET = 61;
/** <b>Byte</b> offset into AT state data where 'mode' variable (long) is stored. */
public static final int MODE_BYTE_OFFSET = MachineState.HEADER_LENGTH + (MODE_VALUE_OFFSET * MachineState.VALUE_SIZE);
public static class OfferMessageData {
public byte[] partnerRavencoinPKH;
public byte[] hashOfSecretA;
public long lockTimeA;
}
public static final int OFFER_MESSAGE_LENGTH = 20 /*partnerRavencoinPKH*/ + 20 /*hashOfSecretA*/ + 8 /*lockTimeA*/;
public static final int TRADE_MESSAGE_LENGTH = 32 /*partner's Qortal trade address (padded from 25 to 32)*/
+ 24 /*partner's Ravencoin PKH (padded from 20 to 24)*/
+ 8 /*AT trade timeout (minutes)*/
+ 24 /*hash of secret-A (padded from 20 to 24)*/
+ 8 /*lockTimeA*/;
public static final int REDEEM_MESSAGE_LENGTH = 32 /*secret-A*/ + 32 /*partner's Qortal receiving address padded from 25 to 32*/;
public static final int CANCEL_MESSAGE_LENGTH = 32 /*AT creator's Qortal address*/;
private static RavencoinACCTv3 instance;
private RavencoinACCTv3() {
}
public static synchronized RavencoinACCTv3 getInstance() {
if (instance == null)
instance = new RavencoinACCTv3();
return instance;
}
@Override
public byte[] getCodeBytesHash() {
return CODE_BYTES_HASH;
}
@Override
public int getModeByteOffset() {
return MODE_BYTE_OFFSET;
}
@Override
public ForeignBlockchain getBlockchain() {
return Ravencoin.getInstance();
}
/**
* Returns Qortal AT creation bytes for cross-chain trading AT.
* <p>
* <tt>tradeTimeout</tt> (minutes) is the time window for the trade partner to send the
* 32-byte secret to the AT, before the AT automatically refunds the AT's creator.
*
* @param creatorTradeAddress AT creator's trade Qortal address
* @param ravencoinPublicKeyHash 20-byte HASH160 of creator's trade Ravencoin public key
* @param qortAmount how much QORT to pay trade partner if they send correct 32-byte secrets to AT
* @param ravencoinAmount how much RVN the AT creator is expecting to trade
* @param tradeTimeout suggested timeout for entire trade
*/
public static byte[] buildQortalAT(String creatorTradeAddress, byte[] ravencoinPublicKeyHash, long qortAmount, long ravencoinAmount, int tradeTimeout) {
if (ravencoinPublicKeyHash.length != 20)
throw new IllegalArgumentException("Ravencoin public key hash should be 20 bytes");
// Labels for data segment addresses
int addrCounter = 0;
// Constants (with corresponding dataByteBuffer.put*() calls below)
final int addrCreatorTradeAddress1 = addrCounter++;
final int addrCreatorTradeAddress2 = addrCounter++;
final int addrCreatorTradeAddress3 = addrCounter++;
final int addrCreatorTradeAddress4 = addrCounter++;
final int addrRavencoinPublicKeyHash = addrCounter;
addrCounter += 4;
final int addrQortAmount = addrCounter++;
final int addrRavencoinAmount = addrCounter++;
final int addrTradeTimeout = addrCounter++;
final int addrMessageTxnType = addrCounter++;
final int addrExpectedTradeMessageLength = addrCounter++;
final int addrExpectedRedeemMessageLength = addrCounter++;
final int addrCreatorAddressPointer = addrCounter++;
final int addrQortalPartnerAddressPointer = addrCounter++;
final int addrMessageSenderPointer = addrCounter++;
final int addrTradeMessagePartnerRavencoinPKHOffset = addrCounter++;
final int addrPartnerRavencoinPKHPointer = addrCounter++;
final int addrTradeMessageHashOfSecretAOffset = addrCounter++;
final int addrHashOfSecretAPointer = addrCounter++;
final int addrRedeemMessageReceivingAddressOffset = addrCounter++;
final int addrMessageDataPointer = addrCounter++;
final int addrMessageDataLength = addrCounter++;
final int addrPartnerReceivingAddressPointer = addrCounter++;
final int addrEndOfConstants = addrCounter;
// Variables
final int addrCreatorAddress1 = addrCounter++;
final int addrCreatorAddress2 = addrCounter++;
final int addrCreatorAddress3 = addrCounter++;
final int addrCreatorAddress4 = addrCounter++;
final int addrQortalPartnerAddress1 = addrCounter++;
final int addrQortalPartnerAddress2 = addrCounter++;
final int addrQortalPartnerAddress3 = addrCounter++;
final int addrQortalPartnerAddress4 = addrCounter++;
final int addrLockTimeA = addrCounter++;
final int addrRefundTimeout = addrCounter++;
final int addrRefundTimestamp = addrCounter++;
final int addrLastTxnTimestamp = addrCounter++;
final int addrBlockTimestamp = addrCounter++;
final int addrTxnType = addrCounter++;
final int addrResult = addrCounter++;
final int addrMessageSender1 = addrCounter++;
final int addrMessageSender2 = addrCounter++;
final int addrMessageSender3 = addrCounter++;
final int addrMessageSender4 = addrCounter++;
final int addrMessageLength = addrCounter++;
final int addrMessageData = addrCounter;
addrCounter += 4;
final int addrHashOfSecretA = addrCounter;
addrCounter += 4;
final int addrPartnerRavencoinPKH = addrCounter;
addrCounter += 4;
final int addrPartnerReceivingAddress = addrCounter;
addrCounter += 4;
final int addrMode = addrCounter++;
assert addrMode == MODE_VALUE_OFFSET : String.format("addrMode %d does not match MODE_VALUE_OFFSET %d", addrMode, MODE_VALUE_OFFSET);
// Data segment
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
// AT creator's trade Qortal address, decoded from Base58
assert dataByteBuffer.position() == addrCreatorTradeAddress1 * MachineState.VALUE_SIZE : "addrCreatorTradeAddress1 incorrect";
byte[] creatorTradeAddressBytes = Base58.decode(creatorTradeAddress);
dataByteBuffer.put(Bytes.ensureCapacity(creatorTradeAddressBytes, 32, 0));
// Ravencoin public key hash
assert dataByteBuffer.position() == addrRavencoinPublicKeyHash * MachineState.VALUE_SIZE : "addrRavencoinPublicKeyHash incorrect";
dataByteBuffer.put(Bytes.ensureCapacity(ravencoinPublicKeyHash, 32, 0));
// Redeem Qort amount
assert dataByteBuffer.position() == addrQortAmount * MachineState.VALUE_SIZE : "addrQortAmount incorrect";
dataByteBuffer.putLong(qortAmount);
// Expected Ravencoin amount
assert dataByteBuffer.position() == addrRavencoinAmount * MachineState.VALUE_SIZE : "addrRavencoinAmount incorrect";
dataByteBuffer.putLong(ravencoinAmount);
// Suggested trade timeout (minutes)
assert dataByteBuffer.position() == addrTradeTimeout * MachineState.VALUE_SIZE : "addrTradeTimeout incorrect";
dataByteBuffer.putLong(tradeTimeout);
// We're only interested in MESSAGE transactions
assert dataByteBuffer.position() == addrMessageTxnType * MachineState.VALUE_SIZE : "addrMessageTxnType incorrect";
dataByteBuffer.putLong(API.ATTransactionType.MESSAGE.value);
// Expected length of 'trade' MESSAGE data from AT creator
assert dataByteBuffer.position() == addrExpectedTradeMessageLength * MachineState.VALUE_SIZE : "addrExpectedTradeMessageLength incorrect";
dataByteBuffer.putLong(TRADE_MESSAGE_LENGTH);
// Expected length of 'redeem' MESSAGE data from trade partner
assert dataByteBuffer.position() == addrExpectedRedeemMessageLength * MachineState.VALUE_SIZE : "addrExpectedRedeemMessageLength incorrect";
dataByteBuffer.putLong(REDEEM_MESSAGE_LENGTH);
// Index into data segment of AT creator's address, used by GET_B_IND
assert dataByteBuffer.position() == addrCreatorAddressPointer * MachineState.VALUE_SIZE : "addrCreatorAddressPointer incorrect";
dataByteBuffer.putLong(addrCreatorAddress1);
// Index into data segment of partner's Qortal address, used by SET_B_IND
assert dataByteBuffer.position() == addrQortalPartnerAddressPointer * MachineState.VALUE_SIZE : "addrQortalPartnerAddressPointer incorrect";
dataByteBuffer.putLong(addrQortalPartnerAddress1);
// Index into data segment of (temporary) transaction's sender's address, used by GET_B_IND
assert dataByteBuffer.position() == addrMessageSenderPointer * MachineState.VALUE_SIZE : "addrMessageSenderPointer incorrect";
dataByteBuffer.putLong(addrMessageSender1);
// Offset into 'trade' MESSAGE data payload for extracting partner's Ravencoin PKH
assert dataByteBuffer.position() == addrTradeMessagePartnerRavencoinPKHOffset * MachineState.VALUE_SIZE : "addrTradeMessagePartnerRavencoinPKHOffset incorrect";
dataByteBuffer.putLong(32L);
// Index into data segment of partner's Ravencoin PKH, used by GET_B_IND
assert dataByteBuffer.position() == addrPartnerRavencoinPKHPointer * MachineState.VALUE_SIZE : "addrPartnerRavencoinPKHPointer incorrect";
dataByteBuffer.putLong(addrPartnerRavencoinPKH);
// Offset into 'trade' MESSAGE data payload for extracting hash-of-secret-A
assert dataByteBuffer.position() == addrTradeMessageHashOfSecretAOffset * MachineState.VALUE_SIZE : "addrTradeMessageHashOfSecretAOffset incorrect";
dataByteBuffer.putLong(64L);
// Index into data segment to hash of secret A, used by GET_B_IND
assert dataByteBuffer.position() == addrHashOfSecretAPointer * MachineState.VALUE_SIZE : "addrHashOfSecretAPointer incorrect";
dataByteBuffer.putLong(addrHashOfSecretA);
// Offset into 'redeem' MESSAGE data payload for extracting Qortal receiving address
assert dataByteBuffer.position() == addrRedeemMessageReceivingAddressOffset * MachineState.VALUE_SIZE : "addrRedeemMessageReceivingAddressOffset incorrect";
dataByteBuffer.putLong(32L);
// Source location and length for hashing any passed secret
assert dataByteBuffer.position() == addrMessageDataPointer * MachineState.VALUE_SIZE : "addrMessageDataPointer incorrect";
dataByteBuffer.putLong(addrMessageData);
assert dataByteBuffer.position() == addrMessageDataLength * MachineState.VALUE_SIZE : "addrMessageDataLength incorrect";
dataByteBuffer.putLong(32L);
// Pointer into data segment of where to save partner's receiving Qortal address, used by GET_B_IND
assert dataByteBuffer.position() == addrPartnerReceivingAddressPointer * MachineState.VALUE_SIZE : "addrPartnerReceivingAddressPointer incorrect";
dataByteBuffer.putLong(addrPartnerReceivingAddress);
assert dataByteBuffer.position() == addrEndOfConstants * MachineState.VALUE_SIZE : "dataByteBuffer position not at end of constants";
// Code labels
Integer labelRefund = null;
Integer labelTradeTxnLoop = null;
Integer labelCheckTradeTxn = null;
Integer labelCheckCancelTxn = null;
Integer labelNotTradeNorCancelTxn = null;
Integer labelCheckNonRefundTradeTxn = null;
Integer labelTradeTxnExtract = null;
Integer labelRedeemTxnLoop = null;
Integer labelCheckRedeemTxn = null;
Integer labelCheckRedeemTxnSender = null;
Integer labelPayout = null;
ByteBuffer codeByteBuffer = ByteBuffer.allocate(768);
// Two-pass version
for (int pass = 0; pass < 2; ++pass) {
codeByteBuffer.clear();
try {
/* Initialization */
// Use AT creation 'timestamp' as starting point for finding transactions sent to AT
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrLastTxnTimestamp));
// Load B register with AT creator's address so we can save it into addrCreatorAddress1-4
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_CREATOR_INTO_B));
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrCreatorAddressPointer));
/* NOP - to ensure RAVENCOIN ACCT is unique */
codeByteBuffer.put(OpCode.NOP.compile());
// Set restart position to after this opcode
codeByteBuffer.put(OpCode.SET_PCS.compile());
/* Loop, waiting for message from AT creator's trade address containing trade partner details, or AT owner's address to cancel offer */
/* Transaction processing loop */
labelTradeTxnLoop = codeByteBuffer.position();
/* Sleep until message arrives */
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.SLEEP_UNTIL_MESSAGE.value, addrLastTxnTimestamp));
// Find next transaction (if any) to this AT since the last one (referenced by addrLastTxnTimestamp)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxnTimestamp));
// If no transaction found, A will be zero. If A is zero, set addrResult to 1, otherwise 0.
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.CHECK_A_IS_ZERO, addrResult));
// If addrResult is zero (i.e. A is non-zero, transaction was found) then go check transaction
codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelCheckTradeTxn)));
// Stop and wait for next block
codeByteBuffer.put(OpCode.STP_IMD.compile());
/* Check transaction */
labelCheckTradeTxn = codeByteBuffer.position();
// Update our 'last found transaction's timestamp' using 'timestamp' from transaction
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A, addrLastTxnTimestamp));
// Extract transaction type (message/payment) from transaction and save type in addrTxnType
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TYPE_FROM_TX_IN_A, addrTxnType));
// If transaction type is not MESSAGE type then go look for another transaction
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxnType, addrMessageTxnType, calcOffset(codeByteBuffer, labelTradeTxnLoop)));
/* Check transaction's sender. We're expecting AT creator's trade address for 'trade' message, or AT creator's own address for 'cancel' message. */
// Extract sender address from transaction into B register
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_ADDRESS_FROM_TX_IN_A_INTO_B));
// Save B register into data segment starting at addrMessageSender1 (as pointed to by addrMessageSenderPointer)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageSenderPointer));
// Compare each part of message sender's address with AT creator's trade address. If they don't match, check for cancel situation.
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrCreatorTradeAddress1, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrCreatorTradeAddress2, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrCreatorTradeAddress3, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrCreatorTradeAddress4, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
// Message sender's address matches AT creator's trade address so go process 'trade' message
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelCheckNonRefundTradeTxn == null ? 0 : labelCheckNonRefundTradeTxn));
/* Checking message sender for possible cancel message */
labelCheckCancelTxn = codeByteBuffer.position();
// Compare each part of message sender's address with AT creator's address. If they don't match, look for another transaction.
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrCreatorAddress1, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrCreatorAddress2, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrCreatorAddress3, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrCreatorAddress4, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
// Partner address is AT creator's address, so cancel offer and finish.
codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.CANCELLED.value));
// We're finished forever (finishing auto-refunds remaining balance to AT creator)
codeByteBuffer.put(OpCode.FIN_IMD.compile());
/* Not trade nor cancel message */
labelNotTradeNorCancelTxn = codeByteBuffer.position();
// Loop to find another transaction
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelTradeTxnLoop == null ? 0 : labelTradeTxnLoop));
/* Possible switch-to-trade-mode message */
labelCheckNonRefundTradeTxn = codeByteBuffer.position();
// Check 'trade' message we received has expected number of message bytes
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(QortalFunctionCode.GET_MESSAGE_LENGTH_FROM_TX_IN_A.value, addrMessageLength));
// If message length matches, branch to info extraction code
codeByteBuffer.put(OpCode.BEQ_DAT.compile(addrMessageLength, addrExpectedTradeMessageLength, calcOffset(codeByteBuffer, labelTradeTxnExtract)));
// Message length didn't match - go back to finding another 'trade' MESSAGE transaction
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelTradeTxnLoop == null ? 0 : labelTradeTxnLoop));
/* Extracting info from 'trade' MESSAGE transaction */
labelTradeTxnExtract = codeByteBuffer.position();
// Extract message from transaction into B register
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B));
// Save B register into data segment starting at addrQortalPartnerAddress1 (as pointed to by addrQortalPartnerAddressPointer)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrQortalPartnerAddressPointer));
// Extract trade partner's Ravencoin public key hash (PKH) from message into B
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessagePartnerRavencoinPKHOffset));
// Store partner's Ravencoin PKH (we only really use values from B1-B3)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerRavencoinPKHPointer));
// Extract AT trade timeout (minutes) (from B4)
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B4, addrRefundTimeout));
// Grab next 32 bytes
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessageHashOfSecretAOffset));
// Extract hash-of-secret-A (we only really use values from B1-B3)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrHashOfSecretAPointer));
// Extract lockTime-A (from B4)
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B4, addrLockTimeA));
// Calculate trade timeout refund 'timestamp' by adding addrRefundTimeout minutes to this transaction's 'timestamp', then save into addrRefundTimestamp
codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.ADD_MINUTES_TO_TIMESTAMP, addrRefundTimestamp, addrLastTxnTimestamp, addrRefundTimeout));
/* We are in 'trade mode' */
codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.TRADING.value));
// Set restart position to after this opcode
codeByteBuffer.put(OpCode.SET_PCS.compile());
/* Loop, waiting for trade timeout or 'redeem' MESSAGE from Qortal trade partner */
// Fetch current block 'timestamp'
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_BLOCK_TIMESTAMP, addrBlockTimestamp));
// If we're not past refund 'timestamp' then look for next transaction
codeByteBuffer.put(OpCode.BLT_DAT.compile(addrBlockTimestamp, addrRefundTimestamp, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
// We're past refund 'timestamp' so go refund everything back to AT creator
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRefund == null ? 0 : labelRefund));
/* Transaction processing loop */
labelRedeemTxnLoop = codeByteBuffer.position();
// Find next transaction to this AT since the last one (if any)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxnTimestamp));
// If no transaction found, A will be zero. If A is zero, set addrComparator to 1, otherwise 0.
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.CHECK_A_IS_ZERO, addrResult));
// If addrResult is zero (i.e. A is non-zero, transaction was found) then go check transaction
codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelCheckRedeemTxn)));
// Stop and wait for next block
codeByteBuffer.put(OpCode.STP_IMD.compile());
/* Check transaction */
labelCheckRedeemTxn = codeByteBuffer.position();
// Update our 'last found transaction's timestamp' using 'timestamp' from transaction
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A, addrLastTxnTimestamp));
// Extract transaction type (message/payment) from transaction and save type in addrTxnType
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TYPE_FROM_TX_IN_A, addrTxnType));
// If transaction type is not MESSAGE type then go look for another transaction
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxnType, addrMessageTxnType, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
/* Check message payload length */
codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(QortalFunctionCode.GET_MESSAGE_LENGTH_FROM_TX_IN_A.value, addrMessageLength));
// If message length matches, branch to sender checking code
codeByteBuffer.put(OpCode.BEQ_DAT.compile(addrMessageLength, addrExpectedRedeemMessageLength, calcOffset(codeByteBuffer, labelCheckRedeemTxnSender)));
// Message length didn't match - go back to finding another 'redeem' MESSAGE transaction
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRedeemTxnLoop == null ? 0 : labelRedeemTxnLoop));
/* Check transaction's sender */
labelCheckRedeemTxnSender = codeByteBuffer.position();
// Extract sender address from transaction into B register
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_ADDRESS_FROM_TX_IN_A_INTO_B));
// Save B register into data segment starting at addrMessageSender1 (as pointed to by addrMessageSenderPointer)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageSenderPointer));
// Compare each part of transaction's sender's address with expected address. If they don't match, look for another transaction.
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrQortalPartnerAddress1, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrQortalPartnerAddress2, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrQortalPartnerAddress3, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrQortalPartnerAddress4, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
/* Check 'secret-A' in transaction's message */
// Extract secret-A from first 32 bytes of message from transaction into B register
codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B));
// Save B register into data segment starting at addrMessageData (as pointed to by addrMessageDataPointer)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageDataPointer));
// Load B register with expected hash result (as pointed to by addrHashOfSecretAPointer)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrHashOfSecretAPointer));
// Perform HASH160 using source data at addrMessageData. (Location and length specified via addrMessageDataPointer and addrMessageDataLength).
// Save the equality result (1 if they match, 0 otherwise) into addrResult.
codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.CHECK_HASH160_WITH_B, addrResult, addrMessageDataPointer, addrMessageDataLength));
// If hashes don't match, addrResult will be zero so go find another transaction
codeByteBuffer.put(OpCode.BNZ_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelPayout)));
codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRedeemTxnLoop == null ? 0 : labelRedeemTxnLoop));
/* Success! Pay arranged amount to receiving address */
labelPayout = codeByteBuffer.position();
// Extract Qortal receiving address from next 32 bytes of message from transaction into B register
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrRedeemMessageReceivingAddressOffset));
// Save B register into data segment starting at addrPartnerReceivingAddress (as pointed to by addrPartnerReceivingAddressPointer)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerReceivingAddressPointer));
// Pay AT's balance to receiving address
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PAY_TO_ADDRESS_IN_B, addrQortAmount));
// Set redeemed mode
codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.REDEEMED.value));
// We're finished forever (finishing auto-refunds remaining balance to AT creator)
codeByteBuffer.put(OpCode.FIN_IMD.compile());
// Fall-through to refunding any remaining balance back to AT creator
/* Refund balance back to AT creator */
labelRefund = codeByteBuffer.position();
// Set refunded mode
codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.REFUNDED.value));
// We're finished forever (finishing auto-refunds remaining balance to AT creator)
codeByteBuffer.put(OpCode.FIN_IMD.compile());
} catch (CompilationException e) {
throw new IllegalStateException("Unable to compile RVN-QORT ACCT?", e);
}
}
codeByteBuffer.flip();
byte[] codeBytes = new byte[codeByteBuffer.limit()];
codeByteBuffer.get(codeBytes);
assert Arrays.equals(Crypto.digest(codeBytes), RavencoinACCTv3.CODE_BYTES_HASH)
: String.format("BTCACCT.CODE_BYTES_HASH mismatch: expected %s, actual %s", HashCode.fromBytes(CODE_BYTES_HASH), HashCode.fromBytes(Crypto.digest(codeBytes)));
final short ciyamAtVersion = 2;
final short numCallStackPages = 0;
final short numUserStackPages = 0;
final long minActivationAmount = 0L;
return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount);
}
/**
* Returns CrossChainTradeData with useful info extracted from AT.
*/
@Override
public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
}
/**
* Returns CrossChainTradeData with useful info extracted from AT.
*/
@Override
public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
}
/**
* Returns CrossChainTradeData with useful info extracted from AT.
*/
public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData) throws DataException {
byte[] addressBytes = new byte[25]; // for general use
String atAddress = atStateData.getATAddress();
CrossChainTradeData tradeData = new CrossChainTradeData();
tradeData.foreignBlockchain = SupportedBlockchain.RAVENCOIN.name();
tradeData.acctName = NAME;
tradeData.qortalAtAddress = atAddress;
tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
tradeData.creationTimestamp = creationTimestamp;
Account atAccount = new Account(repository, atAddress);
tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
byte[] stateData = atStateData.getStateData();
ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData);
dataByteBuffer.position(MachineState.HEADER_LENGTH);
/* Constants */
// Skip creator's trade address
dataByteBuffer.get(addressBytes);
tradeData.qortalCreatorTradeAddress = Base58.encode(addressBytes);
dataByteBuffer.position(dataByteBuffer.position() + 32 - addressBytes.length);
// Creator's Ravencoin/foreign public key hash
tradeData.creatorForeignPKH = new byte[20];
dataByteBuffer.get(tradeData.creatorForeignPKH);
dataByteBuffer.position(dataByteBuffer.position() + 32 - tradeData.creatorForeignPKH.length); // skip to 32 bytes
// We don't use secret-B
tradeData.hashOfSecretB = null;
// Redeem payout
tradeData.qortAmount = dataByteBuffer.getLong();
// Expected RVN amount
tradeData.expectedForeignAmount = dataByteBuffer.getLong();
// Trade timeout
tradeData.tradeTimeout = (int) dataByteBuffer.getLong();
// Skip MESSAGE transaction type
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip expected 'trade' message length
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip expected 'redeem' message length
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip pointer to creator's address
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip pointer to partner's Qortal trade address
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip pointer to message sender
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip 'trade' message data offset for partner's Ravencoin PKH
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip pointer to partner's Ravencoin PKH
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip 'trade' message data offset for hash-of-secret-A
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip pointer to hash-of-secret-A
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip 'redeem' message data offset for partner's Qortal receiving address
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip pointer to message data
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip message data length
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip pointer to partner's receiving address
dataByteBuffer.position(dataByteBuffer.position() + 8);
/* End of constants / begin variables */
// Skip AT creator's address
dataByteBuffer.position(dataByteBuffer.position() + 8 * 4);
// Partner's trade address (if present)
dataByteBuffer.get(addressBytes);
String qortalRecipient = Base58.encode(addressBytes);
dataByteBuffer.position(dataByteBuffer.position() + 32 - addressBytes.length);
// Potential lockTimeA (if in trade mode)
int lockTimeA = (int) dataByteBuffer.getLong();
// AT refund timeout (probably only useful for debugging)
int refundTimeout = (int) dataByteBuffer.getLong();
// Trade-mode refund timestamp (AT 'timestamp' converted to Qortal block height)
long tradeRefundTimestamp = dataByteBuffer.getLong();
// Skip last transaction timestamp
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip block timestamp
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip transaction type
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip temporary result
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip temporary message sender
dataByteBuffer.position(dataByteBuffer.position() + 8 * 4);
// Skip message length
dataByteBuffer.position(dataByteBuffer.position() + 8);
// Skip temporary message data
dataByteBuffer.position(dataByteBuffer.position() + 8 * 4);
// Potential hash160 of secret A
byte[] hashOfSecretA = new byte[20];
dataByteBuffer.get(hashOfSecretA);
dataByteBuffer.position(dataByteBuffer.position() + 32 - hashOfSecretA.length); // skip to 32 bytes
// Potential partner's Ravencoin PKH
byte[] partnerRavencoinPKH = new byte[20];
dataByteBuffer.get(partnerRavencoinPKH);
dataByteBuffer.position(dataByteBuffer.position() + 32 - partnerRavencoinPKH.length); // skip to 32 bytes
// Partner's receiving address (if present)
byte[] partnerReceivingAddress = new byte[25];
dataByteBuffer.get(partnerReceivingAddress);
dataByteBuffer.position(dataByteBuffer.position() + 32 - partnerReceivingAddress.length); // skip to 32 bytes
// Trade AT's 'mode'
long modeValue = dataByteBuffer.getLong();
AcctMode mode = AcctMode.valueOf((int) (modeValue & 0xffL));
/* End of variables */
if (mode != null && mode != AcctMode.OFFERING) {
tradeData.mode = mode;
tradeData.refundTimeout = refundTimeout;
tradeData.tradeRefundHeight = new Timestamp(tradeRefundTimestamp).blockHeight;
tradeData.qortalPartnerAddress = qortalRecipient;
tradeData.hashOfSecretA = hashOfSecretA;
tradeData.partnerForeignPKH = partnerRavencoinPKH;
tradeData.lockTimeA = lockTimeA;
if (mode == AcctMode.REDEEMED)
tradeData.qortalPartnerReceivingAddress = Base58.encode(partnerReceivingAddress);
} else {
tradeData.mode = AcctMode.OFFERING;
}
tradeData.duplicateDeprecated();
return tradeData;
}
/** Returns 'offer' MESSAGE payload for trade partner to send to AT creator's trade address. */
public static byte[] buildOfferMessage(byte[] partnerBitcoinPKH, byte[] hashOfSecretA, int lockTimeA) {
byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA);
return Bytes.concat(partnerBitcoinPKH, hashOfSecretA, lockTimeABytes);
}
/** Returns info extracted from 'offer' MESSAGE payload sent by trade partner to AT creator's trade address, or null if not valid. */
public static OfferMessageData extractOfferMessageData(byte[] messageData) {
if (messageData == null || messageData.length != OFFER_MESSAGE_LENGTH)
return null;
OfferMessageData offerMessageData = new OfferMessageData();
offerMessageData.partnerRavencoinPKH = Arrays.copyOfRange(messageData, 0, 20);
offerMessageData.hashOfSecretA = Arrays.copyOfRange(messageData, 20, 40);
offerMessageData.lockTimeA = BitTwiddling.longFromBEBytes(messageData, 40);
return offerMessageData;
}
/** Returns 'trade' MESSAGE payload for AT creator to send to AT. */
public static byte[] buildTradeMessage(String partnerQortalTradeAddress, byte[] partnerBitcoinPKH, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
byte[] data = new byte[TRADE_MESSAGE_LENGTH];
byte[] partnerQortalAddressBytes = Base58.decode(partnerQortalTradeAddress);
byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA);
byte[] refundTimeoutBytes = BitTwiddling.toBEByteArray((long) refundTimeout);
System.arraycopy(partnerQortalAddressBytes, 0, data, 0, partnerQortalAddressBytes.length);
System.arraycopy(partnerBitcoinPKH, 0, data, 32, partnerBitcoinPKH.length);
System.arraycopy(refundTimeoutBytes, 0, data, 56, refundTimeoutBytes.length);
System.arraycopy(hashOfSecretA, 0, data, 64, hashOfSecretA.length);
System.arraycopy(lockTimeABytes, 0, data, 88, lockTimeABytes.length);
return data;
}
/** Returns 'cancel' MESSAGE payload for AT creator to cancel trade AT. */
@Override
public byte[] buildCancelMessage(String creatorQortalAddress) {
byte[] data = new byte[CANCEL_MESSAGE_LENGTH];
byte[] creatorQortalAddressBytes = Base58.decode(creatorQortalAddress);
System.arraycopy(creatorQortalAddressBytes, 0, data, 0, creatorQortalAddressBytes.length);
return data;
}
/** Returns 'redeem' MESSAGE payload for trade partner to send to AT. */
public static byte[] buildRedeemMessage(byte[] secretA, String qortalReceivingAddress) {
byte[] data = new byte[REDEEM_MESSAGE_LENGTH];
byte[] qortalReceivingAddressBytes = Base58.decode(qortalReceivingAddress);
System.arraycopy(secretA, 0, data, 0, secretA.length);
System.arraycopy(qortalReceivingAddressBytes, 0, data, 32, qortalReceivingAddressBytes.length);
return data;
}
/** Returns refund timeout (minutes) based on trade partner's 'offer' MESSAGE timestamp and P2SH-A locktime. */
public static int calcRefundTimeout(long offerMessageTimestamp, int lockTimeA) {
// refund should be triggered halfway between offerMessageTimestamp and lockTimeA
return (int) ((lockTimeA - (offerMessageTimestamp / 1000L)) / 2L / 60L);
}
@Override
public byte[] findSecretA(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException {
String atAddress = crossChainTradeData.qortalAtAddress;
String redeemerAddress = crossChainTradeData.qortalPartnerAddress;
// We don't have partner's public key so we check every message to AT
List<MessageTransactionData> messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, atAddress, null, null, null);
if (messageTransactionsData == null)
return null;
// Find 'redeem' message
for (MessageTransactionData messageTransactionData : messageTransactionsData) {
// Check message payload type/encryption
if (messageTransactionData.isText() || messageTransactionData.isEncrypted())
continue;
// Check message payload size
byte[] messageData = messageTransactionData.getData();
if (messageData.length != REDEEM_MESSAGE_LENGTH)
// Wrong payload length
continue;
// Check sender
if (!Crypto.toAddress(messageTransactionData.getSenderPublicKey()).equals(redeemerAddress))
// Wrong sender;
continue;
// Extract secretA
byte[] secretA = new byte[32];
System.arraycopy(messageData, 0, secretA, 0, secretA.length);
byte[] hashOfSecretA = Crypto.hash160(secretA);
if (!Arrays.equals(hashOfSecretA, crossChainTradeData.hashOfSecretA))
continue;
return secretA;
}
return null;
}
}

View File

@@ -57,6 +57,34 @@ public enum SupportedBlockchain {
public ACCT getLatestAcct() {
return DogecoinACCTv3.getInstance();
}
},
DIGIBYTE(Arrays.asList(
Triple.valueOf(DigibyteACCTv3.NAME, DigibyteACCTv3.CODE_BYTES_HASH, DigibyteACCTv3::getInstance)
)) {
@Override
public ForeignBlockchain getInstance() {
return Digibyte.getInstance();
}
@Override
public ACCT getLatestAcct() {
return DigibyteACCTv3.getInstance();
}
},
RAVENCOIN(Arrays.asList(
Triple.valueOf(RavencoinACCTv3.NAME, RavencoinACCTv3.CODE_BYTES_HASH, RavencoinACCTv3::getInstance)
)) {
@Override
public ForeignBlockchain getInstance() {
return Ravencoin.getInstance();
}
@Override
public ACCT getLatestAcct() {
return RavencoinACCTv3.getInstance();
}
};
private static final Map<ByteArray, Supplier<ACCT>> supportedAcctsByCodeHash = Arrays.stream(SupportedBlockchain.values())

View File

@@ -195,7 +195,7 @@ public class Name {
this.repository.getNameRepository().save(this.nameData);
}
public void buy(BuyNameTransactionData buyNameTransactionData) throws DataException {
public void buy(BuyNameTransactionData buyNameTransactionData, boolean modifyBalances) throws DataException {
// Save previous name-changing reference in this transaction's data
// Caller is expected to save
buyNameTransactionData.setNameReference(this.nameData.getReference());
@@ -203,15 +203,20 @@ public class Name {
// Mark not for-sale but leave price in case we want to orphan
this.nameData.setIsForSale(false);
// Update seller's balance
Account seller = new Account(this.repository, this.nameData.getOwner());
seller.modifyAssetBalance(Asset.QORT, buyNameTransactionData.getAmount());
if (modifyBalances) {
// Update seller's balance
Account seller = new Account(this.repository, this.nameData.getOwner());
seller.modifyAssetBalance(Asset.QORT, buyNameTransactionData.getAmount());
}
// Set new owner
Account buyer = new PublicKeyAccount(this.repository, buyNameTransactionData.getBuyerPublicKey());
this.nameData.setOwner(buyer.getAddress());
// Update buyer's balance
buyer.modifyAssetBalance(Asset.QORT, - buyNameTransactionData.getAmount());
if (modifyBalances) {
// Update buyer's balance
buyer.modifyAssetBalance(Asset.QORT, -buyNameTransactionData.getAmount());
}
// Set name-changing reference to this transaction
this.nameData.setReference(buyNameTransactionData.getSignature());

View File

@@ -13,7 +13,7 @@ import org.qortal.crypto.MemoryPoW;
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.network.message.MessageType;
import org.qortal.settings.Settings;
import org.qortal.network.message.ResponseMessage;
import org.qortal.utils.DaemonThreadFactory;

View File

@@ -13,6 +13,7 @@ import org.qortal.data.block.BlockData;
import org.qortal.data.network.PeerData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.network.message.*;
import org.qortal.network.task.*;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
@@ -32,6 +33,7 @@ import java.nio.channels.*;
import java.security.SecureRandom;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
@@ -41,9 +43,8 @@ import java.util.stream.Collectors;
// For managing peers
public class Network {
private static final Logger LOGGER = LogManager.getLogger(Network.class);
private static Network instance;
private static final int LISTEN_BACKLOG = 10;
private static final int LISTEN_BACKLOG = 5;
/**
* How long before retrying after a connection failure, in milliseconds.
*/
@@ -122,14 +123,8 @@ public class Network {
private final ExecuteProduceConsume networkEPC;
private Selector channelSelector;
private ServerSocketChannel serverChannel;
private Iterator<SelectionKey> channelIterator = null;
// volatile because value is updated inside any one of the EPC threads
private volatile long nextConnectTaskTimestamp = 0L; // ms - try first connect once NTP syncs
private final ExecutorService broadcastExecutor = Executors.newCachedThreadPool();
// volatile because value is updated inside any one of the EPC threads
private volatile long nextBroadcastTimestamp = 0L; // ms - try first broadcast once NTP syncs
private SelectionKey serverSelectionKey;
private final Set<SelectableChannel> channelsPendingWrite = ConcurrentHashMap.newKeySet();
private final Lock mergePeersLock = new ReentrantLock();
@@ -137,6 +132,8 @@ public class Network {
private String ourExternalIpAddress = null;
private int ourExternalPort = Settings.getInstance().getListenPort();
private volatile boolean isShuttingDown = false;
// Constructors
private Network() {
@@ -170,7 +167,7 @@ public class Network {
serverChannel.configureBlocking(false);
serverChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);
serverChannel.bind(endpoint, LISTEN_BACKLOG);
serverChannel.register(channelSelector, SelectionKey.OP_ACCEPT);
serverSelectionKey = serverChannel.register(channelSelector, SelectionKey.OP_ACCEPT);
} catch (UnknownHostException e) {
LOGGER.error("Can't bind listen socket to address {}", Settings.getInstance().getBindAddress());
throw new IOException("Can't bind listen socket to address", e);
@@ -180,7 +177,8 @@ public class Network {
}
// Load all known peers from repository
synchronized (this.allKnownPeers) { List<String> fixedNetwork = Settings.getInstance().getFixedNetwork();
synchronized (this.allKnownPeers) {
List<String> fixedNetwork = Settings.getInstance().getFixedNetwork();
if (fixedNetwork != null && !fixedNetwork.isEmpty()) {
Long addedWhen = NTP.getTime();
String addedBy = "fixedNetwork";
@@ -214,12 +212,16 @@ public class Network {
// Getters / setters
public static synchronized Network getInstance() {
if (instance == null) {
instance = new Network();
}
private static class SingletonContainer {
private static final Network INSTANCE = new Network();
}
return instance;
public static Network getInstance() {
return SingletonContainer.INSTANCE;
}
public int getMaxPeers() {
return this.maxPeers;
}
public byte[] getMessageMagic() {
@@ -453,6 +455,11 @@ public class Network {
class NetworkProcessor extends ExecuteProduceConsume {
private final AtomicLong nextConnectTaskTimestamp = new AtomicLong(0L); // ms - try first connect once NTP syncs
private final AtomicLong nextBroadcastTimestamp = new AtomicLong(0L); // ms - try first broadcast once NTP syncs
private Iterator<SelectionKey> channelIterator = null;
NetworkProcessor(ExecutorService executor) {
super(executor);
}
@@ -494,43 +501,23 @@ public class Network {
}
private Task maybeProducePeerMessageTask() {
for (Peer peer : getImmutableConnectedPeers()) {
Task peerTask = peer.getMessageTask();
if (peerTask != null) {
return peerTask;
}
}
return null;
return getImmutableConnectedPeers().stream()
.map(Peer::getMessageTask)
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
}
private Task maybeProducePeerPingTask(Long now) {
// Ask connected peers whether they need a ping
for (Peer peer : getImmutableHandshakedPeers()) {
Task peerTask = peer.getPingTask(now);
if (peerTask != null) {
return peerTask;
}
}
return null;
}
class PeerConnectTask implements ExecuteProduceConsume.Task {
private final Peer peer;
PeerConnectTask(Peer peer) {
this.peer = peer;
}
@Override
public void perform() throws InterruptedException {
connectPeer(peer);
}
return getImmutableHandshakedPeers().stream()
.map(peer -> peer.getPingTask(now))
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
}
private Task maybeProduceConnectPeerTask(Long now) throws InterruptedException {
if (now == null || now < nextConnectTaskTimestamp) {
if (now == null || now < nextConnectTaskTimestamp.get()) {
return null;
}
@@ -538,7 +525,7 @@ public class Network {
return null;
}
nextConnectTaskTimestamp = now + 1000L;
nextConnectTaskTimestamp.set(now + 1000L);
Peer targetPeer = getConnectablePeer(now);
if (targetPeer == null) {
@@ -550,66 +537,15 @@ public class Network {
}
private Task maybeProduceBroadcastTask(Long now) {
if (now == null || now < nextBroadcastTimestamp) {
if (now == null || now < nextBroadcastTimestamp.get()) {
return null;
}
nextBroadcastTimestamp = now + BROADCAST_INTERVAL;
return () -> Controller.getInstance().doNetworkBroadcast();
}
class ChannelTask implements ExecuteProduceConsume.Task {
private final SelectionKey selectionKey;
ChannelTask(SelectionKey selectionKey) {
this.selectionKey = selectionKey;
}
@Override
public void perform() throws InterruptedException {
try {
LOGGER.trace("Thread {} has pending channel: {}, with ops {}",
Thread.currentThread().getId(), selectionKey.channel(), selectionKey.readyOps());
// process pending channel task
if (selectionKey.isReadable()) {
connectionRead((SocketChannel) selectionKey.channel());
} else if (selectionKey.isAcceptable()) {
acceptConnection((ServerSocketChannel) selectionKey.channel());
}
LOGGER.trace("Thread {} processed channel: {}",
Thread.currentThread().getId(), selectionKey.channel());
} catch (CancelledKeyException e) {
LOGGER.trace("Thread {} encountered cancelled channel: {}",
Thread.currentThread().getId(), selectionKey.channel());
}
}
private void connectionRead(SocketChannel socketChannel) {
Peer peer = getPeerFromChannel(socketChannel);
if (peer == null) {
return;
}
try {
peer.readChannel();
} catch (IOException e) {
if (e.getMessage() != null && e.getMessage().toLowerCase().contains("connection reset")) {
peer.disconnect("Connection reset");
return;
}
LOGGER.trace("[{}] Network thread {} encountered I/O error: {}", peer.getPeerConnectionId(),
Thread.currentThread().getId(), e.getMessage(), e);
peer.disconnect("I/O error");
}
}
nextBroadcastTimestamp.set(now + BROADCAST_INTERVAL);
return new BroadcastTask();
}
private Task maybeProduceChannelTask(boolean canBlock) throws InterruptedException {
final SelectionKey nextSelectionKey;
// Synchronization here to enforce thread-safety on channelIterator
synchronized (channelSelector) {
// anything to do?
@@ -630,91 +566,73 @@ public class Network {
}
channelIterator = channelSelector.selectedKeys().iterator();
LOGGER.trace("Thread {}, after {} select, channelIterator now {}",
Thread.currentThread().getId(),
canBlock ? "blocking": "non-blocking",
channelIterator);
}
if (channelIterator.hasNext()) {
nextSelectionKey = channelIterator.next();
channelIterator.remove();
} else {
nextSelectionKey = null;
if (!channelIterator.hasNext()) {
channelIterator = null; // Nothing to do so reset iterator to cause new select
LOGGER.trace("Thread {}, channelIterator now null", Thread.currentThread().getId());
return null;
}
LOGGER.trace("Thread {}, nextSelectionKey {}, channelIterator now {}",
Thread.currentThread().getId(), nextSelectionKey, channelIterator);
}
final SelectionKey nextSelectionKey = channelIterator.next();
channelIterator.remove();
if (nextSelectionKey == null) {
return null;
}
// Just in case underlying socket channel already closed elsewhere, etc.
if (!nextSelectionKey.isValid())
return null;
return new ChannelTask(nextSelectionKey);
}
}
LOGGER.trace("Thread {}, nextSelectionKey {}", Thread.currentThread().getId(), nextSelectionKey);
private void acceptConnection(ServerSocketChannel serverSocketChannel) throws InterruptedException {
SocketChannel socketChannel;
SelectableChannel socketChannel = nextSelectionKey.channel();
try {
socketChannel = serverSocketChannel.accept();
} catch (IOException e) {
return;
}
// No connection actually accepted?
if (socketChannel == null) {
return;
}
PeerAddress address = PeerAddress.fromSocket(socketChannel.socket());
List<String> fixedNetwork = Settings.getInstance().getFixedNetwork();
if (fixedNetwork != null && !fixedNetwork.isEmpty() && ipNotInFixedList(address, fixedNetwork)) {
try {
LOGGER.debug("Connection discarded from peer {} as not in the fixed network list", address);
socketChannel.close();
} catch (IOException e) {
// IGNORE
}
return;
}
final Long now = NTP.getTime();
Peer newPeer;
try {
if (now == null) {
LOGGER.debug("Connection discarded from peer {} due to lack of NTP sync", address);
socketChannel.close();
return;
}
if (getImmutableConnectedPeers().size() >= maxPeers) {
// We have enough peers
LOGGER.debug("Connection discarded from peer {} because the server is full", address);
socketChannel.close();
return;
}
LOGGER.debug("Connection accepted from peer {}", address);
newPeer = new Peer(socketChannel, channelSelector);
this.addConnectedPeer(newPeer);
} catch (IOException e) {
if (socketChannel.isOpen()) {
try {
LOGGER.debug("Connection failed from peer {} while connecting/closing", address);
socketChannel.close();
} catch (IOException ce) {
// Couldn't close?
if (nextSelectionKey.isReadable()) {
clearInterestOps(nextSelectionKey, SelectionKey.OP_READ);
Peer peer = getPeerFromChannel((SocketChannel) socketChannel);
if (peer == null)
return null;
return new ChannelReadTask((SocketChannel) socketChannel, peer);
}
if (nextSelectionKey.isWritable()) {
clearInterestOps(nextSelectionKey, SelectionKey.OP_WRITE);
Peer peer = getPeerFromChannel((SocketChannel) socketChannel);
if (peer == null)
return null;
// Any thread that queues a message to send can set OP_WRITE,
// but we only allow one pending/active ChannelWriteTask per Peer
if (!channelsPendingWrite.add(socketChannel))
return null;
return new ChannelWriteTask((SocketChannel) socketChannel, peer);
}
if (nextSelectionKey.isAcceptable()) {
clearInterestOps(nextSelectionKey, SelectionKey.OP_ACCEPT);
return new ChannelAcceptTask((ServerSocketChannel) socketChannel);
}
} catch (CancelledKeyException e) {
/*
* Sometimes nextSelectionKey is cancelled / becomes invalid between the isValid() test at line 586
* and later calls to isReadable() / isWritable() / isAcceptable() which themselves call isValid()!
* Those isXXXable() calls could throw CancelledKeyException, so we catch it here and return null.
*/
return null;
}
}
return;
}
this.onPeerReady(newPeer);
return null;
}
}
private boolean ipNotInFixedList(PeerAddress address, List<String> fixedNetwork) {
public boolean ipNotInFixedList(PeerAddress address, List<String> fixedNetwork) {
for (String ipAddress : fixedNetwork) {
String[] bits = ipAddress.split(":");
if (bits.length >= 1 && bits.length <= 2 && address.getHost().equals(bits[0])) {
@@ -750,8 +668,9 @@ public class Network {
peers.removeIf(isConnectedPeer);
// Don't consider already connected peers (resolved address match)
// XXX This might be too slow if we end up waiting a long time for hostnames to resolve via DNS
peers.removeIf(isResolvedAsConnectedPeer);
// Disabled because this might be too slow if we end up waiting a long time for hostnames to resolve via DNS
// Which is ok because duplicate connections to the same peer are handled during handshaking
// peers.removeIf(isResolvedAsConnectedPeer);
this.checkLongestConnection(now);
@@ -781,8 +700,12 @@ public class Network {
}
}
private boolean connectPeer(Peer newPeer) throws InterruptedException {
SocketChannel socketChannel = newPeer.connect(this.channelSelector);
public boolean connectPeer(Peer newPeer) throws InterruptedException {
// Also checked before creating PeerConnectTask
if (getImmutableOutboundHandshakedPeers().size() >= minOutboundPeers)
return false;
SocketChannel socketChannel = newPeer.connect();
if (socketChannel == null) {
return false;
}
@@ -797,7 +720,7 @@ public class Network {
return true;
}
private Peer getPeerFromChannel(SocketChannel socketChannel) {
public Peer getPeerFromChannel(SocketChannel socketChannel) {
for (Peer peer : this.getImmutableConnectedPeers()) {
if (peer.getSocketChannel() == socketChannel) {
return peer;
@@ -830,7 +753,74 @@ public class Network {
nextDisconnectionCheck = now + DISCONNECTION_CHECK_INTERVAL;
}
// Peer callbacks
// SocketChannel interest-ops manipulations
private static final String[] OP_NAMES = new String[SelectionKey.OP_ACCEPT * 2];
static {
for (int i = 0; i < OP_NAMES.length; i++) {
StringJoiner joiner = new StringJoiner(",");
if ((i & SelectionKey.OP_READ) != 0) joiner.add("OP_READ");
if ((i & SelectionKey.OP_WRITE) != 0) joiner.add("OP_WRITE");
if ((i & SelectionKey.OP_CONNECT) != 0) joiner.add("OP_CONNECT");
if ((i & SelectionKey.OP_ACCEPT) != 0) joiner.add("OP_ACCEPT");
OP_NAMES[i] = joiner.toString();
}
}
public void clearInterestOps(SelectableChannel socketChannel, int interestOps) {
SelectionKey selectionKey = socketChannel.keyFor(channelSelector);
if (selectionKey == null)
return;
clearInterestOps(selectionKey, interestOps);
}
private void clearInterestOps(SelectionKey selectionKey, int interestOps) {
if (!selectionKey.channel().isOpen())
return;
LOGGER.trace("Thread {} clearing {} interest-ops on channel: {}",
Thread.currentThread().getId(),
OP_NAMES[interestOps],
selectionKey.channel());
selectionKey.interestOpsAnd(~interestOps);
}
public void setInterestOps(SelectableChannel socketChannel, int interestOps) {
SelectionKey selectionKey = socketChannel.keyFor(channelSelector);
if (selectionKey == null) {
try {
selectionKey = socketChannel.register(this.channelSelector, interestOps);
} catch (ClosedChannelException e) {
// Channel already closed so ignore
return;
}
// Fall-through to allow logging
}
setInterestOps(selectionKey, interestOps);
}
private void setInterestOps(SelectionKey selectionKey, int interestOps) {
if (!selectionKey.channel().isOpen())
return;
LOGGER.trace("Thread {} setting {} interest-ops on channel: {}",
Thread.currentThread().getId(),
OP_NAMES[interestOps],
selectionKey.channel());
selectionKey.interestOpsOr(interestOps);
}
// Peer / Task callbacks
public void notifyChannelNotWriting(SelectableChannel socketChannel) {
this.channelsPendingWrite.remove(socketChannel);
}
protected void wakeupChannelSelector() {
this.channelSelector.wakeup();
@@ -856,8 +846,6 @@ public class Network {
}
public void onDisconnect(Peer peer) {
// Notify Controller
Controller.getInstance().onPeerDisconnect(peer);
if (peer.getConnectionEstablishedTime() > 0L) {
LOGGER.debug("[{}] Disconnected from peer {}", peer.getPeerConnectionId(), peer);
} else {
@@ -865,6 +853,25 @@ public class Network {
}
this.removeConnectedPeer(peer);
this.channelsPendingWrite.remove(peer.getSocketChannel());
if (this.isShuttingDown)
// No need to do any further processing, like re-enabling listen socket or notifying Controller
return;
if (getImmutableConnectedPeers().size() < maxPeers - 1
&& serverSelectionKey.isValid()
&& (serverSelectionKey.interestOps() & SelectionKey.OP_ACCEPT) == 0) {
try {
LOGGER.debug("Re-enabling accepting incoming connections because the server is not longer full");
setInterestOps(serverSelectionKey, SelectionKey.OP_ACCEPT);
} catch (CancelledKeyException e) {
LOGGER.error("Failed to re-enable accepting of incoming connections: {}", e.getMessage());
}
}
// Notify Controller
Controller.getInstance().onPeerDisconnect(peer);
}
public void peerMisbehaved(Peer peer) {
@@ -1302,8 +1309,9 @@ public class Network {
try {
InetSocketAddress knownAddress = peerAddress.toSocketAddress();
List<Peer> peers = this.getImmutableConnectedPeers();
peers.removeIf(peer -> !Peer.addressEquals(knownAddress, peer.getResolvedAddress()));
List<Peer> peers = this.getImmutableConnectedPeers().stream()
.filter(peer -> Peer.addressEquals(knownAddress, peer.getResolvedAddress()))
.collect(Collectors.toList());
for (Peer peer : peers) {
peer.disconnect("to be forgotten");
@@ -1461,54 +1469,27 @@ public class Network {
}
public void broadcast(Function<Peer, Message> peerMessageBuilder) {
class Broadcaster implements Runnable {
private final Random random = new Random();
for (Peer peer : getImmutableHandshakedPeers()) {
if (this.isShuttingDown)
return;
private List<Peer> targetPeers;
private Function<Peer, Message> peerMessageBuilder;
Message message = peerMessageBuilder.apply(peer);
Broadcaster(List<Peer> targetPeers, Function<Peer, Message> peerMessageBuilder) {
this.targetPeers = targetPeers;
this.peerMessageBuilder = peerMessageBuilder;
if (message == null) {
continue;
}
@Override
public void run() {
Thread.currentThread().setName("Network Broadcast");
for (Peer peer : targetPeers) {
// Very short sleep to reduce strain, improve multi-threading and catch interrupts
try {
Thread.sleep(random.nextInt(20) + 20L);
} catch (InterruptedException e) {
break;
}
Message message = peerMessageBuilder.apply(peer);
if (message == null) {
continue;
}
if (!peer.sendMessage(message)) {
peer.disconnect("failed to broadcast message");
}
}
Thread.currentThread().setName("Network Broadcast (dormant)");
if (!peer.sendMessage(message)) {
peer.disconnect("failed to broadcast message");
}
}
try {
broadcastExecutor.execute(new Broadcaster(this.getImmutableHandshakedPeers(), peerMessageBuilder));
} catch (RejectedExecutionException e) {
// Can't execute - probably because we're shutting down, so ignore
}
}
// Shutdown
public void shutdown() {
this.isShuttingDown = true;
// Close listen socket to prevent more incoming connections
if (this.serverChannel.isOpen()) {
try {
@@ -1527,16 +1508,6 @@ public class Network {
LOGGER.warn("Interrupted while waiting for networking threads to terminate");
}
// Stop broadcasts
this.broadcastExecutor.shutdownNow();
try {
if (!this.broadcastExecutor.awaitTermination(1000, TimeUnit.MILLISECONDS)) {
LOGGER.warn("Broadcast threads failed to terminate");
}
} catch (InterruptedException e) {
LOGGER.warn("Interrupted while waiting for broadcast threads failed to terminate");
}
// Close all peer connections
for (Peer peer : this.getImmutableConnectedPeers()) {
peer.shutdown();

View File

@@ -11,25 +11,21 @@ import org.qortal.data.network.PeerChainTipData;
import org.qortal.data.network.PeerData;
import org.qortal.network.message.ChallengeMessage;
import org.qortal.network.message.Message;
import org.qortal.network.message.Message.MessageException;
import org.qortal.network.message.Message.MessageType;
import org.qortal.network.message.PingMessage;
import org.qortal.network.message.MessageException;
import org.qortal.network.task.MessageTask;
import org.qortal.network.task.PingTask;
import org.qortal.settings.Settings;
import org.qortal.utils.ExecuteProduceConsume;
import org.qortal.utils.ExecuteProduceConsume.Task;
import org.qortal.utils.NTP;
import java.io.IOException;
import java.net.*;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.security.SecureRandom;
import java.util.*;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -48,9 +44,9 @@ public class Peer {
private static final int RESPONSE_TIMEOUT = 3000; // ms
/**
* Maximum time to wait for a peer to respond with blocks (ms)
* Maximum time to wait for a message to be added to sendQueue (ms)
*/
public static final int FETCH_BLOCKS_TIMEOUT = 10000;
private static final int QUEUE_TIMEOUT = 1000; // ms
/**
* Interval between PING messages to a peer. (ms)
@@ -71,10 +67,14 @@ public class Peer {
private final UUID peerConnectionId = UUID.randomUUID();
private final Object byteBufferLock = new Object();
private ByteBuffer byteBuffer;
private Map<Integer, BlockingQueue<Message>> replyQueues;
private LinkedBlockingQueue<Message> pendingMessages;
private TransferQueue<Message> sendQueue;
private ByteBuffer outputBuffer;
private String outputMessageType;
private int outputMessageId;
/**
* True if we created connection to peer, false if we accepted incoming connection from peer.
*/
@@ -98,7 +98,7 @@ public class Peer {
/**
* When last PING message was sent, or null if pings not started yet.
*/
private Long lastPingSent;
private Long lastPingSent = null;
byte[] ourChallenge;
@@ -160,10 +160,10 @@ public class Peer {
/**
* Construct Peer using existing, connected socket
*/
public Peer(SocketChannel socketChannel, Selector channelSelector) throws IOException {
public Peer(SocketChannel socketChannel) throws IOException {
this.isOutbound = false;
this.socketChannel = socketChannel;
sharedSetup(channelSelector);
sharedSetup();
this.resolvedAddress = ((InetSocketAddress) socketChannel.socket().getRemoteSocketAddress());
this.isLocal = isAddressLocal(this.resolvedAddress.getAddress());
@@ -276,7 +276,7 @@ public class Peer {
}
}
protected void setLastPing(long lastPing) {
public void setLastPing(long lastPing) {
synchronized (this.peerInfoLock) {
this.lastPing = lastPing;
}
@@ -346,12 +346,6 @@ public class Peer {
}
}
protected void queueMessage(Message message) {
if (!this.pendingMessages.offer(message)) {
LOGGER.info("[{}] No room to queue message from peer {} - discarding", this.peerConnectionId, this);
}
}
public boolean isSyncInProgress() {
return this.syncInProgress;
}
@@ -396,13 +390,14 @@ public class Peer {
// Processing
private void sharedSetup(Selector channelSelector) throws IOException {
private void sharedSetup() throws IOException {
this.connectionTimestamp = NTP.getTime();
this.socketChannel.setOption(StandardSocketOptions.TCP_NODELAY, true);
this.socketChannel.configureBlocking(false);
this.socketChannel.register(channelSelector, SelectionKey.OP_READ);
Network.getInstance().setInterestOps(this.socketChannel, SelectionKey.OP_READ);
this.byteBuffer = null; // Defer allocation to when we need it, to save memory. Sorry GC!
this.replyQueues = Collections.synchronizedMap(new HashMap<Integer, BlockingQueue<Message>>());
this.sendQueue = new LinkedTransferQueue<>();
this.replyQueues = new ConcurrentHashMap<>();
this.pendingMessages = new LinkedBlockingQueue<>();
Random random = new SecureRandom();
@@ -410,7 +405,7 @@ public class Peer {
random.nextBytes(this.ourChallenge);
}
public SocketChannel connect(Selector channelSelector) {
public SocketChannel connect() {
LOGGER.trace("[{}] Connecting to peer {}", this.peerConnectionId, this);
try {
@@ -418,6 +413,8 @@ public class Peer {
this.isLocal = isAddressLocal(this.resolvedAddress.getAddress());
this.socketChannel = SocketChannel.open();
InetAddress bindAddr = InetAddress.getByName(Settings.getInstance().getBindAddress());
this.socketChannel.socket().bind(new InetSocketAddress(bindAddr, 0));
this.socketChannel.socket().connect(resolvedAddress, CONNECT_TIMEOUT);
} catch (SocketTimeoutException e) {
LOGGER.trace("[{}] Connection timed out to peer {}", this.peerConnectionId, this);
@@ -432,7 +429,7 @@ public class Peer {
try {
LOGGER.debug("[{}] Connected to peer {}", this.peerConnectionId, this);
sharedSetup(channelSelector);
sharedSetup();
return socketChannel;
} catch (IOException e) {
LOGGER.trace("[{}] Post-connection setup failed, peer {}", this.peerConnectionId, this);
@@ -450,7 +447,7 @@ public class Peer {
*
* @throws IOException If this channel is not yet connected
*/
protected void readChannel() throws IOException {
public void readChannel() throws IOException {
synchronized (this.byteBufferLock) {
while (true) {
if (!this.socketChannel.isOpen() || this.socketChannel.socket().isClosed()) {
@@ -556,7 +553,67 @@ public class Peer {
}
}
protected ExecuteProduceConsume.Task getMessageTask() {
/** Maybe send some pending outgoing messages.
*
* @return true if more data is pending to be sent
*/
public boolean writeChannel() throws IOException {
// It is the responsibility of ChannelWriteTask's producer to produce only one call to writeChannel() at a time
while (true) {
// If output byte buffer is null, fetch next message from queue (if any)
while (this.outputBuffer == null) {
Message message;
try {
// Allow other thread time to add message to queue having raised OP_WRITE.
// Timeout is overkill but not excessive enough to clog up networking / EPC.
// This is to avoid race condition in sendMessageWithTimeout() below.
message = this.sendQueue.poll(QUEUE_TIMEOUT, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
// Shutdown situation
return false;
}
// No message? No further work to be done
if (message == null)
return false;
try {
this.outputBuffer = ByteBuffer.wrap(message.toBytes());
this.outputMessageType = message.getType().name();
this.outputMessageId = message.getId();
LOGGER.trace("[{}] Sending {} message with ID {} to peer {}",
this.peerConnectionId, this.outputMessageType, this.outputMessageId, this);
} catch (MessageException e) {
// Something went wrong converting message to bytes, so discard but allow another round
LOGGER.warn("[{}] Failed to send {} message with ID {} to peer {}: {}", this.peerConnectionId,
message.getType().name(), message.getId(), this, e.getMessage());
}
}
// If output byte buffer is not null, send from that
int bytesWritten = this.socketChannel.write(outputBuffer);
LOGGER.trace("[{}] Sent {} bytes of {} message with ID {} to peer {} ({} total)", this.peerConnectionId,
bytesWritten, this.outputMessageType, this.outputMessageId, this, outputBuffer.limit());
// If we've sent 0 bytes then socket buffer is full so we need to wait until it's empty again
if (bytesWritten == 0) {
return true;
}
// If we then exhaust the byte buffer, set it to null (otherwise loop and try to send more)
if (!this.outputBuffer.hasRemaining()) {
this.outputMessageType = null;
this.outputMessageId = 0;
this.outputBuffer = null;
}
}
}
protected Task getMessageTask() {
/*
* If we are still handshaking and there is a message yet to be processed then
* don't produce another message task. This allows us to process handshake
@@ -580,7 +637,7 @@ public class Peer {
}
// Return a task to process message in queue
return () -> Network.getInstance().onMessage(this, nextMessage);
return new MessageTask(this, nextMessage);
}
/**
@@ -605,54 +662,25 @@ public class Peer {
}
try {
// Send message
LOGGER.trace("[{}] Sending {} message with ID {} to peer {}", this.peerConnectionId,
// Queue message, to be picked up by ChannelWriteTask and then peer.writeChannel()
LOGGER.trace("[{}] Queuing {} message with ID {} to peer {}", this.peerConnectionId,
message.getType().name(), message.getId(), this);
ByteBuffer outputBuffer = ByteBuffer.wrap(message.toBytes());
// Check message properly constructed
message.checkValidOutgoing();
synchronized (this.socketChannel) {
final long sendStart = System.currentTimeMillis();
long totalBytes = 0;
while (outputBuffer.hasRemaining()) {
int bytesWritten = this.socketChannel.write(outputBuffer);
totalBytes += bytesWritten;
LOGGER.trace("[{}] Sent {} bytes of {} message with ID {} to peer {} ({} total)", this.peerConnectionId,
bytesWritten, message.getType().name(), message.getId(), this, totalBytes);
if (bytesWritten == 0) {
// Underlying socket's internal buffer probably full,
// so wait a short while for bytes to actually be transmitted over the wire
/*
* NOSONAR squid:S2276 - we don't want to use this.socketChannel.wait()
* as this releases the lock held by synchronized() above
* and would allow another thread to send another message,
* potentially interleaving them on-the-wire, causing checksum failures
* and connection loss.
*/
Thread.sleep(1L); //NOSONAR squid:S2276
if (System.currentTimeMillis() - sendStart > timeout) {
// We've taken too long to send this message
return false;
}
}
}
}
} catch (MessageException e) {
LOGGER.warn("[{}] Failed to send {} message with ID {} to peer {}: {}", this.peerConnectionId,
message.getType().name(), message.getId(), this, e.getMessage());
return false;
} catch (IOException | InterruptedException e) {
// Possible race condition:
// We set OP_WRITE, EPC creates ChannelWriteTask which calls Peer.writeChannel, writeChannel's poll() finds no message to send
// Avoided by poll-with-timeout in writeChannel() above.
Network.getInstance().setInterestOps(this.socketChannel, SelectionKey.OP_WRITE);
return this.sendQueue.tryTransfer(message, timeout, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
// Send failure
return false;
} catch (MessageException e) {
LOGGER.error(e.getMessage(), e);
return false;
}
// Sent OK
return true;
}
/**
@@ -720,7 +748,7 @@ public class Peer {
this.lastPingSent = NTP.getTime();
}
protected ExecuteProduceConsume.Task getPingTask(Long now) {
protected Task getPingTask(Long now) {
// Pings not enabled yet?
if (now == null || this.lastPingSent == null) {
return null;
@@ -734,19 +762,7 @@ public class Peer {
// Not strictly true, but prevents this peer from being immediately chosen again
this.lastPingSent = now;
return () -> {
PingMessage pingMessage = new PingMessage();
Message message = this.getResponse(pingMessage);
if (message == null || message.getType() != MessageType.PING) {
LOGGER.debug("[{}] Didn't receive reply from {} for PING ID {}", this.peerConnectionId, this,
pingMessage.getId());
this.disconnect("no ping received");
return;
}
this.setLastPing(NTP.getTime() - now);
};
return new PingTask(this, now);
}
public void disconnect(String reason) {

View File

@@ -9,38 +9,59 @@ import org.qortal.utils.Serialization;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
public class ArbitraryDataFileListMessage extends Message {
private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
private static final int HASH_LENGTH = Transformer.SHA256_LENGTH;
private static final int MAX_PEER_ADDRESS_LENGTH = PeerData.MAX_PEER_ADDRESS_SIZE;
private final byte[] signature;
private final List<byte[]> hashes;
private byte[] signature;
private List<byte[]> hashes;
private Long requestTime;
private Integer requestHops;
private String peerAddress;
private Boolean isRelayPossible;
public ArbitraryDataFileListMessage(byte[] signature, List<byte[]> hashes, Long requestTime,
Integer requestHops, String peerAddress, boolean isRelayPossible) {
Integer requestHops, String peerAddress, Boolean isRelayPossible) {
super(MessageType.ARBITRARY_DATA_FILE_LIST);
this.signature = signature;
this.hashes = hashes;
this.requestTime = requestTime;
this.requestHops = requestHops;
this.peerAddress = peerAddress;
this.isRelayPossible = isRelayPossible;
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try {
bytes.write(signature);
bytes.write(Ints.toByteArray(hashes.size()));
for (byte[] hash : hashes) {
bytes.write(hash);
}
if (requestTime != null) {
// The remaining fields are optional
bytes.write(Longs.toByteArray(requestTime));
bytes.write(Ints.toByteArray(requestHops));
Serialization.serializeSizedStringV2(bytes, peerAddress);
bytes.write(Ints.toByteArray(Boolean.TRUE.equals(isRelayPossible) ? 1 : 0));
}
} catch (IOException e) {
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
}
this.dataBytes = bytes.toByteArray();
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
public ArbitraryDataFileListMessage(int id, byte[] signature, List<byte[]> hashes, Long requestTime,
/** Legacy version */
public ArbitraryDataFileListMessage(byte[] signature, List<byte[]> hashes) {
this(signature, hashes, null, null, null, null);
}
private ArbitraryDataFileListMessage(int id, byte[] signature, List<byte[]> hashes, Long requestTime,
Integer requestHops, String peerAddress, boolean isRelayPossible) {
super(id, MessageType.ARBITRARY_DATA_FILE_LIST);
@@ -52,24 +73,39 @@ public class ArbitraryDataFileListMessage extends Message {
this.isRelayPossible = isRelayPossible;
}
public List<byte[]> getHashes() {
return this.hashes;
}
public byte[] getSignature() {
return this.signature;
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException, TransformationException {
byte[] signature = new byte[SIGNATURE_LENGTH];
public List<byte[]> getHashes() {
return this.hashes;
}
public Long getRequestTime() {
return this.requestTime;
}
public Integer getRequestHops() {
return this.requestHops;
}
public String getPeerAddress() {
return this.peerAddress;
}
public Boolean isRelayPossible() {
return this.isRelayPossible;
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws MessageException {
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
bytes.get(signature);
int count = bytes.getInt();
List<byte[]> hashes = new ArrayList<>();
for (int i = 0; i < count; ++i) {
byte[] hash = new byte[HASH_LENGTH];
byte[] hash = new byte[Transformer.SHA256_LENGTH];
bytes.get(hash);
hashes.add(hash);
}
@@ -80,99 +116,21 @@ public class ArbitraryDataFileListMessage extends Message {
boolean isRelayPossible = true; // Legacy versions only send this message when relaying is possible
// The remaining fields are optional
if (bytes.hasRemaining()) {
try {
requestTime = bytes.getLong();
requestTime = bytes.getLong();
requestHops = bytes.getInt();
requestHops = bytes.getInt();
peerAddress = Serialization.deserializeSizedStringV2(bytes, MAX_PEER_ADDRESS_LENGTH);
isRelayPossible = bytes.getInt() > 0;
peerAddress = Serialization.deserializeSizedStringV2(bytes, PeerData.MAX_PEER_ADDRESS_SIZE);
isRelayPossible = bytes.getInt() > 0;
} catch (TransformationException e) {
throw new MessageException(e.getMessage(), e);
}
}
return new ArbitraryDataFileListMessage(id, signature, hashes, requestTime, requestHops, peerAddress, isRelayPossible);
}
@Override
protected byte[] toData() {
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(this.signature);
bytes.write(Ints.toByteArray(this.hashes.size()));
for (byte[] hash : this.hashes) {
bytes.write(hash);
}
if (this.requestTime == null) { // To maintain backwards support
return bytes.toByteArray();
}
// The remaining fields are optional
bytes.write(Longs.toByteArray(this.requestTime));
bytes.write(Ints.toByteArray(this.requestHops));
Serialization.serializeSizedStringV2(bytes, this.peerAddress);
bytes.write(Ints.toByteArray(this.isRelayPossible ? 1 : 0));
return bytes.toByteArray();
} catch (IOException e) {
return null;
}
}
public ArbitraryDataFileListMessage cloneWithNewId(int newId) {
ArbitraryDataFileListMessage clone = new ArbitraryDataFileListMessage(this.signature, this.hashes,
this.requestTime, this.requestHops, this.peerAddress, this.isRelayPossible);
clone.setId(newId);
return clone;
}
public void removeOptionalStats() {
this.requestTime = null;
this.requestHops = null;
this.peerAddress = null;
this.isRelayPossible = null;
}
public Long getRequestTime() {
return this.requestTime;
}
public void setRequestTime(Long requestTime) {
this.requestTime = requestTime;
}
public Integer getRequestHops() {
return this.requestHops;
}
public void setRequestHops(Integer requestHops) {
this.requestHops = requestHops;
}
public String getPeerAddress() {
return this.peerAddress;
}
public void setPeerAddress(String peerAddress) {
this.peerAddress = peerAddress;
}
public Boolean isRelayPossible() {
return this.isRelayPossible;
}
public void setIsRelayPossible(Boolean isRelayPossible) {
this.isRelayPossible = isRelayPossible;
}
}

View File

@@ -9,44 +9,60 @@ import org.qortal.transform.Transformer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
public class ArbitraryDataFileMessage extends Message {
private static final Logger LOGGER = LogManager.getLogger(ArbitraryDataFileMessage.class);
private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
private final byte[] signature;
private final ArbitraryDataFile arbitraryDataFile;
private byte[] signature;
private ArbitraryDataFile arbitraryDataFile;
public ArbitraryDataFileMessage(byte[] signature, ArbitraryDataFile arbitraryDataFile) {
super(MessageType.ARBITRARY_DATA_FILE);
this.signature = signature;
this.arbitraryDataFile = arbitraryDataFile;
byte[] data = arbitraryDataFile.getBytes();
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try {
bytes.write(signature);
bytes.write(Ints.toByteArray(data.length));
bytes.write(data);
} catch (IOException e) {
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
}
this.dataBytes = bytes.toByteArray();
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
public ArbitraryDataFileMessage(int id, byte[] signature, ArbitraryDataFile arbitraryDataFile) {
private ArbitraryDataFileMessage(int id, byte[] signature, ArbitraryDataFile arbitraryDataFile) {
super(id, MessageType.ARBITRARY_DATA_FILE);
this.signature = signature;
this.arbitraryDataFile = arbitraryDataFile;
}
public byte[] getSignature() {
return this.signature;
}
public ArbitraryDataFile getArbitraryDataFile() {
return this.arbitraryDataFile;
}
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws UnsupportedEncodingException {
byte[] signature = new byte[SIGNATURE_LENGTH];
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws MessageException {
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
byteBuffer.get(signature);
int dataLength = byteBuffer.getInt();
if (byteBuffer.remaining() != dataLength)
return null;
if (byteBuffer.remaining() < dataLength)
throw new BufferUnderflowException();
byte[] data = new byte[dataLength];
byteBuffer.get(data);
@@ -54,43 +70,10 @@ public class ArbitraryDataFileMessage extends Message {
try {
ArbitraryDataFile arbitraryDataFile = new ArbitraryDataFile(data, signature);
return new ArbitraryDataFileMessage(id, signature, arbitraryDataFile);
}
catch (DataException e) {
} catch (DataException e) {
LOGGER.info("Unable to process received file: {}", e.getMessage());
return null;
throw new MessageException("Unable to process received file: " + e.getMessage(), e);
}
}
@Override
protected byte[] toData() {
if (this.arbitraryDataFile == null) {
return null;
}
byte[] data = this.arbitraryDataFile.getBytes();
if (data == null) {
return null;
}
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(signature);
bytes.write(Ints.toByteArray(data.length));
bytes.write(data);
return bytes.toByteArray();
} catch (IOException e) {
return null;
}
}
public ArbitraryDataFileMessage cloneWithNewId(int newId) {
ArbitraryDataFileMessage clone = new ArbitraryDataFileMessage(this.signature, this.arbitraryDataFile);
clone.setId(newId);
return clone;
}
}

View File

@@ -2,7 +2,7 @@ package org.qortal.network.message;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import org.qortal.transform.Transformer;
@@ -11,13 +11,26 @@ import com.google.common.primitives.Ints;
public class ArbitraryDataMessage extends Message {
private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
private byte[] signature;
private byte[] data;
public ArbitraryDataMessage(byte[] signature, byte[] data) {
this(-1, signature, data);
super(MessageType.ARBITRARY_DATA);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try {
bytes.write(signature);
bytes.write(Ints.toByteArray(data.length));
bytes.write(data);
} catch (IOException e) {
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
}
this.dataBytes = bytes.toByteArray();
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private ArbitraryDataMessage(int id, byte[] signature, byte[] data) {
@@ -35,14 +48,14 @@ public class ArbitraryDataMessage extends Message {
return this.data;
}
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws UnsupportedEncodingException {
byte[] signature = new byte[SIGNATURE_LENGTH];
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) {
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
byteBuffer.get(signature);
int dataLength = byteBuffer.getInt();
if (byteBuffer.remaining() != dataLength)
return null;
if (byteBuffer.remaining() < dataLength)
throw new BufferUnderflowException();
byte[] data = new byte[dataLength];
byteBuffer.get(data);
@@ -50,24 +63,4 @@ public class ArbitraryDataMessage extends Message {
return new ArbitraryDataMessage(id, signature, data);
}
@Override
protected byte[] toData() {
if (this.data == null)
return null;
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(this.signature);
bytes.write(Ints.toByteArray(this.data.length));
bytes.write(this.data);
return bytes.toByteArray();
} catch (IOException e) {
return null;
}
}
}

View File

@@ -7,28 +7,40 @@ import org.qortal.transform.Transformer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
public class ArbitraryMetadataMessage extends Message {
private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
private byte[] signature;
private ArbitraryDataFile arbitraryMetadataFile;
private final byte[] signature;
private final ArbitraryDataFile arbitraryMetadataFile;
public ArbitraryMetadataMessage(byte[] signature, ArbitraryDataFile arbitraryDataFile) {
public ArbitraryMetadataMessage(byte[] signature, ArbitraryDataFile arbitraryMetadataFile) {
super(MessageType.ARBITRARY_METADATA);
this.signature = signature;
this.arbitraryMetadataFile = arbitraryDataFile;
byte[] data = arbitraryMetadataFile.getBytes();
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try {
bytes.write(signature);
bytes.write(Ints.toByteArray(data.length));
bytes.write(data);
} catch (IOException e) {
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
}
this.dataBytes = bytes.toByteArray();
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
public ArbitraryMetadataMessage(int id, byte[] signature, ArbitraryDataFile arbitraryDataFile) {
private ArbitraryMetadataMessage(int id, byte[] signature, ArbitraryDataFile arbitraryMetadataFile) {
super(id, MessageType.ARBITRARY_METADATA);
this.signature = signature;
this.arbitraryMetadataFile = arbitraryDataFile;
this.arbitraryMetadataFile = arbitraryMetadataFile;
}
public byte[] getSignature() {
@@ -39,14 +51,14 @@ public class ArbitraryMetadataMessage extends Message {
return this.arbitraryMetadataFile;
}
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws UnsupportedEncodingException {
byte[] signature = new byte[SIGNATURE_LENGTH];
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws MessageException {
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
byteBuffer.get(signature);
int dataLength = byteBuffer.getInt();
if (byteBuffer.remaining() != dataLength)
return null;
if (byteBuffer.remaining() < dataLength)
throw new BufferUnderflowException();
byte[] data = new byte[dataLength];
byteBuffer.get(data);
@@ -54,42 +66,9 @@ public class ArbitraryMetadataMessage extends Message {
try {
ArbitraryDataFile arbitraryMetadataFile = new ArbitraryDataFile(data, signature);
return new ArbitraryMetadataMessage(id, signature, arbitraryMetadataFile);
} catch (DataException e) {
throw new MessageException("Unable to process arbitrary metadata message: " + e.getMessage(), e);
}
catch (DataException e) {
return null;
}
}
@Override
protected byte[] toData() {
if (this.arbitraryMetadataFile == null) {
return null;
}
byte[] data = this.arbitraryMetadataFile.getBytes();
if (data == null) {
return null;
}
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(signature);
bytes.write(Ints.toByteArray(data.length));
bytes.write(data);
return bytes.toByteArray();
} catch (IOException e) {
return null;
}
}
public ArbitraryMetadataMessage cloneWithNewId(int newId) {
ArbitraryMetadataMessage clone = new ArbitraryMetadataMessage(this.signature, this.arbitraryMetadataFile);
clone.setId(newId);
return clone;
}
}

View File

@@ -8,21 +8,37 @@ import org.qortal.utils.Serialization;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
public class ArbitrarySignaturesMessage extends Message {
private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
private String peerAddress;
private int requestHops;
private List<byte[]> signatures;
public ArbitrarySignaturesMessage(String peerAddress, int requestHops, List<byte[]> signatures) {
this(-1, peerAddress, requestHops, signatures);
super(MessageType.ARBITRARY_SIGNATURES);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try {
Serialization.serializeSizedStringV2(bytes, peerAddress);
bytes.write(Ints.toByteArray(requestHops));
bytes.write(Ints.toByteArray(signatures.size()));
for (byte[] signature : signatures)
bytes.write(signature);
} catch (IOException e) {
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
}
this.dataBytes = bytes.toByteArray();
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private ArbitrarySignaturesMessage(int id, String peerAddress, int requestHops, List<byte[]> signatures) {
@@ -41,27 +57,24 @@ public class ArbitrarySignaturesMessage extends Message {
return this.signatures;
}
public int getRequestHops() {
return this.requestHops;
}
public void setRequestHops(int requestHops) {
this.requestHops = requestHops;
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException, TransformationException {
String peerAddress = Serialization.deserializeSizedStringV2(bytes, PeerData.MAX_PEER_ADDRESS_SIZE);
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws MessageException {
String peerAddress;
try {
peerAddress = Serialization.deserializeSizedStringV2(bytes, PeerData.MAX_PEER_ADDRESS_SIZE);
} catch (TransformationException e) {
throw new MessageException(e.getMessage(), e);
}
int requestHops = bytes.getInt();
int signatureCount = bytes.getInt();
if (bytes.remaining() != signatureCount * SIGNATURE_LENGTH)
return null;
if (bytes.remaining() < signatureCount * Transformer.SIGNATURE_LENGTH)
throw new BufferUnderflowException();
List<byte[]> signatures = new ArrayList<>();
for (int i = 0; i < signatureCount; ++i) {
byte[] signature = new byte[SIGNATURE_LENGTH];
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
bytes.get(signature);
signatures.add(signature);
}
@@ -69,24 +82,4 @@ public class ArbitrarySignaturesMessage extends Message {
return new ArbitrarySignaturesMessage(id, peerAddress, requestHops, signatures);
}
@Override
protected byte[] toData() {
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
Serialization.serializeSizedStringV2(bytes, this.peerAddress);
bytes.write(Ints.toByteArray(this.requestHops));
bytes.write(Ints.toByteArray(this.signatures.size()));
for (byte[] signature : this.signatures)
bytes.write(signature);
return bytes.toByteArray();
} catch (IOException e) {
return null;
}
}
}

View File

@@ -1,14 +1,10 @@
package org.qortal.network.message;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.List;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.block.Block;
import org.qortal.data.at.ATStateData;
import org.qortal.data.block.BlockData;
import org.qortal.data.transaction.TransactionData;
@@ -16,27 +12,15 @@ import org.qortal.transform.TransformationException;
import org.qortal.transform.block.BlockTransformer;
import org.qortal.utils.Triple;
import com.google.common.primitives.Ints;
public class BlockMessage extends Message {
private static final Logger LOGGER = LogManager.getLogger(BlockMessage.class);
private Block block = null;
private final BlockData blockData;
private final List<TransactionData> transactions;
private final List<ATStateData> atStates;
private BlockData blockData = null;
private List<TransactionData> transactions = null;
private List<ATStateData> atStates = null;
private int height;
public BlockMessage(Block block) {
super(MessageType.BLOCK);
this.block = block;
this.blockData = block.getBlockData();
this.height = block.getBlockData().getHeight();
}
// No public constructor as we're an incoming-only message type.
private BlockMessage(int id, BlockData blockData, List<TransactionData> transactions, List<ATStateData> atStates) {
super(id, MessageType.BLOCK);
@@ -44,8 +28,6 @@ public class BlockMessage extends Message {
this.blockData = blockData;
this.transactions = transactions;
this.atStates = atStates;
this.height = blockData.getHeight();
}
public BlockData getBlockData() {
@@ -60,7 +42,7 @@ public class BlockMessage extends Message {
return this.atStates;
}
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws UnsupportedEncodingException {
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws MessageException {
try {
int height = byteBuffer.getInt();
@@ -72,32 +54,8 @@ public class BlockMessage extends Message {
return new BlockMessage(id, blockData, blockInfo.getB(), blockInfo.getC());
} catch (TransformationException e) {
LOGGER.info(String.format("Received garbled BLOCK message: %s", e.getMessage()));
return null;
throw new MessageException(e.getMessage(), e);
}
}
@Override
protected byte[] toData() {
if (this.block == null)
return null;
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(Ints.toByteArray(this.height));
bytes.write(BlockTransformer.toBytes(this.block));
return bytes.toByteArray();
} catch (TransformationException | IOException e) {
return null;
}
}
public BlockMessage cloneWithNewId(int newId) {
BlockMessage clone = new BlockMessage(this.block);
clone.setId(newId);
return clone;
}
}

View File

@@ -2,7 +2,7 @@ package org.qortal.network.message;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
@@ -20,7 +20,25 @@ public class BlockSummariesMessage extends Message {
private List<BlockSummaryData> blockSummaries;
public BlockSummariesMessage(List<BlockSummaryData> blockSummaries) {
this(-1, blockSummaries);
super(MessageType.BLOCK_SUMMARIES);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try {
bytes.write(Ints.toByteArray(blockSummaries.size()));
for (BlockSummaryData blockSummary : blockSummaries) {
bytes.write(Ints.toByteArray(blockSummary.getHeight()));
bytes.write(blockSummary.getSignature());
bytes.write(blockSummary.getMinterPublicKey());
bytes.write(Ints.toByteArray(blockSummary.getOnlineAccountsCount()));
}
} catch (IOException e) {
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
}
this.dataBytes = bytes.toByteArray();
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private BlockSummariesMessage(int id, List<BlockSummaryData> blockSummaries) {
@@ -33,11 +51,11 @@ public class BlockSummariesMessage extends Message {
return this.blockSummaries;
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
int count = bytes.getInt();
if (bytes.remaining() != count * BLOCK_SUMMARY_LENGTH)
return null;
if (bytes.remaining() < count * BLOCK_SUMMARY_LENGTH)
throw new BufferUnderflowException();
List<BlockSummaryData> blockSummaries = new ArrayList<>();
for (int i = 0; i < count; ++i) {
@@ -58,24 +76,4 @@ public class BlockSummariesMessage extends Message {
return new BlockSummariesMessage(id, blockSummaries);
}
@Override
protected byte[] toData() {
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(Ints.toByteArray(this.blockSummaries.size()));
for (BlockSummaryData blockSummary : this.blockSummaries) {
bytes.write(Ints.toByteArray(blockSummary.getHeight()));
bytes.write(blockSummary.getSignature());
bytes.write(blockSummary.getMinterPublicKey());
bytes.write(Ints.toByteArray(blockSummary.getOnlineAccountsCount()));
}
return bytes.toByteArray();
} catch (IOException e) {
return null;
}
}
}

View File

@@ -2,7 +2,6 @@ package org.qortal.network.message;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import org.qortal.block.Block;
@@ -12,59 +11,34 @@ import org.qortal.transform.block.BlockTransformer;
import com.google.common.primitives.Ints;
// This is an OUTGOING-only Message which more readily lends itself to being cached
public class CachedBlockMessage extends Message {
public class CachedBlockMessage extends Message implements Cloneable {
private Block block = null;
private byte[] cachedBytes = null;
public CachedBlockMessage(Block block) {
public CachedBlockMessage(Block block) throws TransformationException {
super(MessageType.BLOCK);
this.block = block;
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try {
bytes.write(Ints.toByteArray(block.getBlockData().getHeight()));
bytes.write(BlockTransformer.toBytes(block));
} catch (IOException e) {
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
}
this.dataBytes = bytes.toByteArray();
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
public CachedBlockMessage(byte[] cachedBytes) {
super(MessageType.BLOCK);
this.block = null;
this.cachedBytes = cachedBytes;
this.dataBytes = cachedBytes;
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws UnsupportedEncodingException {
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) {
throw new UnsupportedOperationException("CachedBlockMessage is for outgoing messages only");
}
@Override
protected byte[] toData() {
// Already serialized?
if (this.cachedBytes != null)
return cachedBytes;
if (this.block == null)
return null;
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(Ints.toByteArray(this.block.getBlockData().getHeight()));
bytes.write(BlockTransformer.toBytes(this.block));
this.cachedBytes = bytes.toByteArray();
// We no longer need source Block
// and Block contains repository handle which is highly likely to be invalid after this call
this.block = null;
return this.cachedBytes;
} catch (TransformationException | IOException e) {
return null;
}
}
public CachedBlockMessage cloneWithNewId(int newId) {
CachedBlockMessage clone = new CachedBlockMessage(this.cachedBytes);
clone.setId(newId);
return clone;
}
}

View File

@@ -10,8 +10,25 @@ public class ChallengeMessage extends Message {
public static final int CHALLENGE_LENGTH = 32;
private final byte[] publicKey;
private final byte[] challenge;
private byte[] publicKey;
private byte[] challenge;
public ChallengeMessage(byte[] publicKey, byte[] challenge) {
super(MessageType.CHALLENGE);
ByteArrayOutputStream bytes = new ByteArrayOutputStream(publicKey.length + challenge.length);
try {
bytes.write(publicKey);
bytes.write(challenge);
} catch (IOException e) {
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
}
this.dataBytes = bytes.toByteArray();
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private ChallengeMessage(int id, byte[] publicKey, byte[] challenge) {
super(id, MessageType.CHALLENGE);
@@ -20,10 +37,6 @@ public class ChallengeMessage extends Message {
this.challenge = challenge;
}
public ChallengeMessage(byte[] publicKey, byte[] challenge) {
this(-1, publicKey, challenge);
}
public byte[] getPublicKey() {
return this.publicKey;
}
@@ -42,15 +55,4 @@ public class ChallengeMessage extends Message {
return new ChallengeMessage(id, publicKey, challenge);
}
@Override
protected byte[] toData() throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(this.publicKey);
bytes.write(this.challenge);
return bytes.toByteArray();
}
}

View File

@@ -5,33 +5,54 @@ import com.google.common.primitives.Longs;
import org.qortal.data.network.PeerData;
import org.qortal.transform.TransformationException;
import org.qortal.transform.Transformer;
import org.qortal.transform.transaction.TransactionTransformer;
import org.qortal.utils.Serialization;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import static org.qortal.transform.Transformer.INT_LENGTH;
import static org.qortal.transform.Transformer.LONG_LENGTH;
public class GetArbitraryDataFileListMessage extends Message {
private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
private static final int HASH_LENGTH = TransactionTransformer.SHA256_LENGTH;
private static final int MAX_PEER_ADDRESS_LENGTH = PeerData.MAX_PEER_ADDRESS_SIZE;
private final byte[] signature;
private byte[] signature;
private List<byte[]> hashes;
private final long requestTime;
private long requestTime;
private int requestHops;
private String requestingPeer;
public GetArbitraryDataFileListMessage(byte[] signature, List<byte[]> hashes, long requestTime, int requestHops, String requestingPeer) {
this(-1, signature, hashes, requestTime, requestHops, requestingPeer);
super(MessageType.GET_ARBITRARY_DATA_FILE_LIST);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try {
bytes.write(signature);
bytes.write(Longs.toByteArray(requestTime));
bytes.write(Ints.toByteArray(requestHops));
if (hashes != null) {
bytes.write(Ints.toByteArray(hashes.size()));
for (byte[] hash : hashes) {
bytes.write(hash);
}
}
else {
bytes.write(Ints.toByteArray(0));
}
if (requestingPeer != null) {
Serialization.serializeSizedStringV2(bytes, requestingPeer);
}
} catch (IOException e) {
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
}
this.dataBytes = bytes.toByteArray();
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private GetArbitraryDataFileListMessage(int id, byte[] signature, List<byte[]> hashes, long requestTime, int requestHops, String requestingPeer) {
@@ -52,8 +73,20 @@ public class GetArbitraryDataFileListMessage extends Message {
return this.hashes;
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException, TransformationException {
byte[] signature = new byte[SIGNATURE_LENGTH];
public long getRequestTime() {
return this.requestTime;
}
public int getRequestHops() {
return this.requestHops;
}
public String getRequestingPeer() {
return this.requestingPeer;
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws MessageException {
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
bytes.get(signature);
@@ -67,7 +100,7 @@ public class GetArbitraryDataFileListMessage extends Message {
hashes = new ArrayList<>();
for (int i = 0; i < hashCount; ++i) {
byte[] hash = new byte[HASH_LENGTH];
byte[] hash = new byte[Transformer.SHA256_LENGTH];
bytes.get(hash);
hashes.add(hash);
}
@@ -75,57 +108,14 @@ public class GetArbitraryDataFileListMessage extends Message {
String requestingPeer = null;
if (bytes.hasRemaining()) {
requestingPeer = Serialization.deserializeSizedStringV2(bytes, MAX_PEER_ADDRESS_LENGTH);
try {
requestingPeer = Serialization.deserializeSizedStringV2(bytes, PeerData.MAX_PEER_ADDRESS_SIZE);
} catch (TransformationException e) {
throw new MessageException(e.getMessage(), e);
}
}
return new GetArbitraryDataFileListMessage(id, signature, hashes, requestTime, requestHops, requestingPeer);
}
@Override
protected byte[] toData() {
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(this.signature);
bytes.write(Longs.toByteArray(this.requestTime));
bytes.write(Ints.toByteArray(this.requestHops));
if (this.hashes != null) {
bytes.write(Ints.toByteArray(this.hashes.size()));
for (byte[] hash : this.hashes) {
bytes.write(hash);
}
}
else {
bytes.write(Ints.toByteArray(0));
}
if (this.requestingPeer != null) {
Serialization.serializeSizedStringV2(bytes, this.requestingPeer);
}
return bytes.toByteArray();
} catch (IOException e) {
return null;
}
}
public long getRequestTime() {
return this.requestTime;
}
public int getRequestHops() {
return this.requestHops;
}
public void setRequestHops(int requestHops) {
this.requestHops = requestHops;
}
public String getRequestingPeer() {
return this.requestingPeer;
}
}

View File

@@ -1,23 +1,31 @@
package org.qortal.network.message;
import org.qortal.transform.Transformer;
import org.qortal.transform.transaction.TransactionTransformer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
public class GetArbitraryDataFileMessage extends Message {
private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
private static final int HASH_LENGTH = TransactionTransformer.SHA256_LENGTH;
private final byte[] signature;
private final byte[] hash;
private byte[] signature;
private byte[] hash;
public GetArbitraryDataFileMessage(byte[] signature, byte[] hash) {
this(-1, signature, hash);
super(MessageType.GET_ARBITRARY_DATA_FILE);
ByteArrayOutputStream bytes = new ByteArrayOutputStream(signature.length + hash.length);
try {
bytes.write(signature);
bytes.write(hash);
} catch (IOException e) {
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
}
this.dataBytes = bytes.toByteArray();
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private GetArbitraryDataFileMessage(int id, byte[] signature, byte[] hash) {
@@ -35,32 +43,14 @@ public class GetArbitraryDataFileMessage extends Message {
return this.hash;
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
if (bytes.remaining() != HASH_LENGTH + SIGNATURE_LENGTH)
return null;
byte[] signature = new byte[SIGNATURE_LENGTH];
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
bytes.get(signature);
byte[] hash = new byte[HASH_LENGTH];
byte[] hash = new byte[Transformer.SHA256_LENGTH];
bytes.get(hash);
return new GetArbitraryDataFileMessage(id, signature, hash);
}
@Override
protected byte[] toData() {
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(this.signature);
bytes.write(this.hash);
return bytes.toByteArray();
} catch (IOException e) {
return null;
}
}
}

View File

@@ -1,20 +1,19 @@
package org.qortal.network.message;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import org.qortal.transform.Transformer;
public class GetArbitraryDataMessage extends Message {
private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
private byte[] signature;
public GetArbitraryDataMessage(byte[] signature) {
this(-1, signature);
super(MessageType.GET_ARBITRARY_DATA);
this.dataBytes = Arrays.copyOf(signature, signature.length);
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private GetArbitraryDataMessage(int id, byte[] signature) {
@@ -27,28 +26,12 @@ public class GetArbitraryDataMessage extends Message {
return this.signature;
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
if (bytes.remaining() != SIGNATURE_LENGTH)
return null;
byte[] signature = new byte[SIGNATURE_LENGTH];
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
bytes.get(signature);
return new GetArbitraryDataMessage(id, signature);
}
@Override
protected byte[] toData() {
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(this.signature);
return bytes.toByteArray();
} catch (IOException e) {
return null;
}
}
}

View File

@@ -6,22 +6,31 @@ import org.qortal.transform.Transformer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import static org.qortal.transform.Transformer.INT_LENGTH;
import static org.qortal.transform.Transformer.LONG_LENGTH;
public class GetArbitraryMetadataMessage extends Message {
private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
private final byte[] signature;
private final long requestTime;
private byte[] signature;
private long requestTime;
private int requestHops;
public GetArbitraryMetadataMessage(byte[] signature, long requestTime, int requestHops) {
this(-1, signature, requestTime, requestHops);
super(MessageType.GET_ARBITRARY_METADATA);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try {
bytes.write(signature);
bytes.write(Longs.toByteArray(requestTime));
bytes.write(Ints.toByteArray(requestHops));
} catch (IOException e) {
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
}
this.dataBytes = bytes.toByteArray();
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private GetArbitraryMetadataMessage(int id, byte[] signature, long requestTime, int requestHops) {
@@ -36,12 +45,16 @@ public class GetArbitraryMetadataMessage extends Message {
return this.signature;
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
if (bytes.remaining() != SIGNATURE_LENGTH + LONG_LENGTH + INT_LENGTH)
return null;
public long getRequestTime() {
return this.requestTime;
}
byte[] signature = new byte[SIGNATURE_LENGTH];
public int getRequestHops() {
return this.requestHops;
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
bytes.get(signature);
long requestTime = bytes.getLong();
@@ -51,33 +64,4 @@ public class GetArbitraryMetadataMessage extends Message {
return new GetArbitraryMetadataMessage(id, signature, requestTime, requestHops);
}
@Override
protected byte[] toData() {
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(this.signature);
bytes.write(Longs.toByteArray(this.requestTime));
bytes.write(Ints.toByteArray(this.requestHops));
return bytes.toByteArray();
} catch (IOException e) {
return null;
}
}
public long getRequestTime() {
return this.requestTime;
}
public int getRequestHops() {
return this.requestHops;
}
public void setRequestHops(int requestHops) {
this.requestHops = requestHops;
}
}

View File

@@ -1,20 +1,19 @@
package org.qortal.network.message;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import org.qortal.transform.block.BlockTransformer;
public class GetBlockMessage extends Message {
private static final int BLOCK_SIGNATURE_LENGTH = BlockTransformer.BLOCK_SIGNATURE_LENGTH;
private byte[] signature;
public GetBlockMessage(byte[] signature) {
this(-1, signature);
super(MessageType.GET_BLOCK);
this.dataBytes = Arrays.copyOf(signature, signature.length);
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private GetBlockMessage(int id, byte[] signature) {
@@ -27,28 +26,11 @@ public class GetBlockMessage extends Message {
return this.signature;
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
if (bytes.remaining() != BLOCK_SIGNATURE_LENGTH)
return null;
byte[] signature = new byte[BLOCK_SIGNATURE_LENGTH];
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
byte[] signature = new byte[BlockTransformer.BLOCK_SIGNATURE_LENGTH];
bytes.get(signature);
return new GetBlockMessage(id, signature);
}
@Override
protected byte[] toData() {
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(this.signature);
return bytes.toByteArray();
} catch (IOException e) {
return null;
}
}
}

View File

@@ -2,23 +2,32 @@ package org.qortal.network.message;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import org.qortal.transform.Transformer;
import org.qortal.transform.block.BlockTransformer;
import com.google.common.primitives.Ints;
public class GetBlockSummariesMessage extends Message {
private static final int BLOCK_SIGNATURE_LENGTH = BlockTransformer.BLOCK_SIGNATURE_LENGTH;
private byte[] parentSignature;
private int numberRequested;
public GetBlockSummariesMessage(byte[] parentSignature, int numberRequested) {
this(-1, parentSignature, numberRequested);
super(MessageType.GET_BLOCK_SUMMARIES);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try {
bytes.write(parentSignature);
bytes.write(Ints.toByteArray(numberRequested));
} catch (IOException e) {
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
}
this.dataBytes = bytes.toByteArray();
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private GetBlockSummariesMessage(int id, byte[] parentSignature, int numberRequested) {
@@ -36,11 +45,8 @@ public class GetBlockSummariesMessage extends Message {
return this.numberRequested;
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
if (bytes.remaining() != BLOCK_SIGNATURE_LENGTH + Transformer.INT_LENGTH)
return null;
byte[] parentSignature = new byte[BLOCK_SIGNATURE_LENGTH];
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
byte[] parentSignature = new byte[BlockTransformer.BLOCK_SIGNATURE_LENGTH];
bytes.get(parentSignature);
int numberRequested = bytes.getInt();
@@ -48,19 +54,4 @@ public class GetBlockSummariesMessage extends Message {
return new GetBlockSummariesMessage(id, parentSignature, numberRequested);
}
@Override
protected byte[] toData() {
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(this.parentSignature);
bytes.write(Ints.toByteArray(this.numberRequested));
return bytes.toByteArray();
} catch (IOException e) {
return null;
}
}
}

View File

@@ -2,7 +2,6 @@ package org.qortal.network.message;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
@@ -20,7 +19,24 @@ public class GetOnlineAccountsMessage extends Message {
private List<OnlineAccountData> onlineAccounts;
public GetOnlineAccountsMessage(List<OnlineAccountData> onlineAccounts) {
this(-1, onlineAccounts);
super(MessageType.GET_ONLINE_ACCOUNTS);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try {
bytes.write(Ints.toByteArray(onlineAccounts.size()));
for (OnlineAccountData onlineAccountData : onlineAccounts) {
bytes.write(Longs.toByteArray(onlineAccountData.getTimestamp()));
bytes.write(onlineAccountData.getPublicKey());
}
} catch (IOException e) {
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
}
this.dataBytes = bytes.toByteArray();
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private GetOnlineAccountsMessage(int id, List<OnlineAccountData> onlineAccounts) {
@@ -33,7 +49,7 @@ public class GetOnlineAccountsMessage extends Message {
return this.onlineAccounts;
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
final int accountCount = bytes.getInt();
List<OnlineAccountData> onlineAccounts = new ArrayList<>(accountCount);
@@ -50,24 +66,4 @@ public class GetOnlineAccountsMessage extends Message {
return new GetOnlineAccountsMessage(id, onlineAccounts);
}
@Override
protected byte[] toData() {
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(Ints.toByteArray(this.onlineAccounts.size()));
for (int i = 0; i < this.onlineAccounts.size(); ++i) {
OnlineAccountData onlineAccountData = this.onlineAccounts.get(i);
bytes.write(Longs.toByteArray(onlineAccountData.getTimestamp()));
bytes.write(onlineAccountData.getPublicKey());
}
return bytes.toByteArray();
} catch (IOException e) {
return null;
}
}
}

View File

@@ -7,7 +7,6 @@ import org.qortal.transform.Transformer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
@@ -24,11 +23,51 @@ import java.util.Map;
* Also V2 only builds online accounts message once!
*/
public class GetOnlineAccountsV2Message extends Message {
private List<OnlineAccountData> onlineAccounts;
private byte[] cachedData;
public GetOnlineAccountsV2Message(List<OnlineAccountData> onlineAccounts) {
this(-1, onlineAccounts);
super(MessageType.GET_ONLINE_ACCOUNTS_V2);
// If we don't have ANY online accounts then it's an easier construction...
if (onlineAccounts.isEmpty()) {
// Always supply a number of accounts
this.dataBytes = Ints.toByteArray(0);
this.checksumBytes = Message.generateChecksum(this.dataBytes);
return;
}
// How many of each timestamp
Map<Long, Integer> countByTimestamp = new HashMap<>();
for (OnlineAccountData onlineAccountData : onlineAccounts) {
Long timestamp = onlineAccountData.getTimestamp();
countByTimestamp.compute(timestamp, (k, v) -> v == null ? 1 : ++v);
}
// We should know exactly how many bytes to allocate now
int byteSize = countByTimestamp.size() * (Transformer.INT_LENGTH + Transformer.TIMESTAMP_LENGTH)
+ onlineAccounts.size() * Transformer.PUBLIC_KEY_LENGTH;
ByteArrayOutputStream bytes = new ByteArrayOutputStream(byteSize);
try {
for (long timestamp : countByTimestamp.keySet()) {
bytes.write(Ints.toByteArray(countByTimestamp.get(timestamp)));
bytes.write(Longs.toByteArray(timestamp));
for (OnlineAccountData onlineAccountData : onlineAccounts) {
if (onlineAccountData.getTimestamp() == timestamp)
bytes.write(onlineAccountData.getPublicKey());
}
}
} catch (IOException e) {
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
}
this.dataBytes = bytes.toByteArray();
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private GetOnlineAccountsV2Message(int id, List<OnlineAccountData> onlineAccounts) {
@@ -41,7 +80,7 @@ public class GetOnlineAccountsV2Message extends Message {
return this.onlineAccounts;
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
int accountCount = bytes.getInt();
List<OnlineAccountData> onlineAccounts = new ArrayList<>(accountCount);
@@ -67,51 +106,4 @@ public class GetOnlineAccountsV2Message extends Message {
return new GetOnlineAccountsV2Message(id, onlineAccounts);
}
@Override
protected synchronized byte[] toData() {
if (this.cachedData != null)
return this.cachedData;
// Shortcut in case we have no online accounts
if (this.onlineAccounts.isEmpty()) {
this.cachedData = Ints.toByteArray(0);
return this.cachedData;
}
// How many of each timestamp
Map<Long, Integer> countByTimestamp = new HashMap<>();
for (int i = 0; i < this.onlineAccounts.size(); ++i) {
OnlineAccountData onlineAccountData = this.onlineAccounts.get(i);
Long timestamp = onlineAccountData.getTimestamp();
countByTimestamp.compute(timestamp, (k, v) -> v == null ? 1 : ++v);
}
// We should know exactly how many bytes to allocate now
int byteSize = countByTimestamp.size() * (Transformer.INT_LENGTH + Transformer.TIMESTAMP_LENGTH)
+ this.onlineAccounts.size() * Transformer.PUBLIC_KEY_LENGTH;
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream(byteSize);
for (long timestamp : countByTimestamp.keySet()) {
bytes.write(Ints.toByteArray(countByTimestamp.get(timestamp)));
bytes.write(Longs.toByteArray(timestamp));
for (int i = 0; i < this.onlineAccounts.size(); ++i) {
OnlineAccountData onlineAccountData = this.onlineAccounts.get(i);
if (onlineAccountData.getTimestamp() == timestamp)
bytes.write(onlineAccountData.getPublicKey());
}
}
this.cachedData = bytes.toByteArray();
return this.cachedData;
} catch (IOException e) {
return null;
}
}
}

View File

@@ -1,25 +1,21 @@
package org.qortal.network.message;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
public class GetPeersMessage extends Message {
public GetPeersMessage() {
this(-1);
super(MessageType.GET_PEERS);
this.dataBytes = EMPTY_DATA_BYTES;
}
private GetPeersMessage(int id) {
super(id, MessageType.GET_PEERS);
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
return new GetPeersMessage(id);
}
@Override
protected byte[] toData() {
return new byte[0];
}
}

View File

@@ -2,24 +2,32 @@ package org.qortal.network.message;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import org.qortal.transform.Transformer;
import org.qortal.transform.block.BlockTransformer;
import com.google.common.primitives.Ints;
public class GetSignaturesV2Message extends Message {
private static final int BLOCK_SIGNATURE_LENGTH = BlockTransformer.BLOCK_SIGNATURE_LENGTH;
private static final int NUMBER_REQUESTED_LENGTH = Transformer.INT_LENGTH;
private byte[] parentSignature;
private int numberRequested;
public GetSignaturesV2Message(byte[] parentSignature, int numberRequested) {
this(-1, parentSignature, numberRequested);
super(MessageType.GET_SIGNATURES_V2);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try {
bytes.write(parentSignature);
bytes.write(Ints.toByteArray(numberRequested));
} catch (IOException e) {
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
}
this.dataBytes = bytes.toByteArray();
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private GetSignaturesV2Message(int id, byte[] parentSignature, int numberRequested) {
@@ -37,11 +45,8 @@ public class GetSignaturesV2Message extends Message {
return this.numberRequested;
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
if (bytes.remaining() != BLOCK_SIGNATURE_LENGTH + NUMBER_REQUESTED_LENGTH)
return null;
byte[] parentSignature = new byte[BLOCK_SIGNATURE_LENGTH];
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
byte[] parentSignature = new byte[BlockTransformer.BLOCK_SIGNATURE_LENGTH];
bytes.get(parentSignature);
int numberRequested = bytes.getInt();
@@ -49,19 +54,4 @@ public class GetSignaturesV2Message extends Message {
return new GetSignaturesV2Message(id, parentSignature, numberRequested);
}
@Override
protected byte[] toData() {
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(this.parentSignature);
bytes.write(Ints.toByteArray(this.numberRequested));
return bytes.toByteArray();
} catch (IOException e) {
return null;
}
}
}

View File

@@ -7,7 +7,6 @@ import org.qortal.transform.Transformer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
@@ -21,10 +20,48 @@ import java.util.Map;
*/
public class GetTradePresencesMessage extends Message {
private List<TradePresenceData> tradePresences;
private byte[] cachedData;
public GetTradePresencesMessage(List<TradePresenceData> tradePresences) {
this(-1, tradePresences);
super(MessageType.GET_TRADE_PRESENCES);
// Shortcut in case we have no trade presences
if (tradePresences.isEmpty()) {
this.dataBytes = Ints.toByteArray(0);
this.checksumBytes = Message.generateChecksum(this.dataBytes);
return;
}
// How many of each timestamp
Map<Long, Integer> countByTimestamp = new HashMap<>();
for (TradePresenceData tradePresenceData : tradePresences) {
Long timestamp = tradePresenceData.getTimestamp();
countByTimestamp.compute(timestamp, (k, v) -> v == null ? 1 : ++v);
}
// We should know exactly how many bytes to allocate now
int byteSize = countByTimestamp.size() * (Transformer.INT_LENGTH + Transformer.TIMESTAMP_LENGTH)
+ tradePresences.size() * Transformer.PUBLIC_KEY_LENGTH;
ByteArrayOutputStream bytes = new ByteArrayOutputStream(byteSize);
try {
for (long timestamp : countByTimestamp.keySet()) {
bytes.write(Ints.toByteArray(countByTimestamp.get(timestamp)));
bytes.write(Longs.toByteArray(timestamp));
for (TradePresenceData tradePresenceData : tradePresences) {
if (tradePresenceData.getTimestamp() == timestamp)
bytes.write(tradePresenceData.getPublicKey());
}
}
} catch (IOException e) {
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
}
this.dataBytes = bytes.toByteArray();
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private GetTradePresencesMessage(int id, List<TradePresenceData> tradePresences) {
@@ -37,7 +74,7 @@ public class GetTradePresencesMessage extends Message {
return this.tradePresences;
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
int groupedEntriesCount = bytes.getInt();
List<TradePresenceData> tradePresences = new ArrayList<>(groupedEntriesCount);
@@ -63,48 +100,4 @@ public class GetTradePresencesMessage extends Message {
return new GetTradePresencesMessage(id, tradePresences);
}
@Override
protected synchronized byte[] toData() {
if (this.cachedData != null)
return this.cachedData;
// Shortcut in case we have no trade presences
if (this.tradePresences.isEmpty()) {
this.cachedData = Ints.toByteArray(0);
return this.cachedData;
}
// How many of each timestamp
Map<Long, Integer> countByTimestamp = new HashMap<>();
for (TradePresenceData tradePresenceData : this.tradePresences) {
Long timestamp = tradePresenceData.getTimestamp();
countByTimestamp.compute(timestamp, (k, v) -> v == null ? 1 : ++v);
}
// We should know exactly how many bytes to allocate now
int byteSize = countByTimestamp.size() * (Transformer.INT_LENGTH + Transformer.TIMESTAMP_LENGTH)
+ this.tradePresences.size() * Transformer.PUBLIC_KEY_LENGTH;
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream(byteSize);
for (long timestamp : countByTimestamp.keySet()) {
bytes.write(Ints.toByteArray(countByTimestamp.get(timestamp)));
bytes.write(Longs.toByteArray(timestamp));
for (TradePresenceData tradePresenceData : this.tradePresences) {
if (tradePresenceData.getTimestamp() == timestamp)
bytes.write(tradePresenceData.getPublicKey());
}
}
this.cachedData = bytes.toByteArray();
return this.cachedData;
} catch (IOException e) {
return null;
}
}
}

View File

@@ -1,20 +1,19 @@
package org.qortal.network.message;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import org.qortal.transform.Transformer;
public class GetTransactionMessage extends Message {
private static final int TRANSACTION_SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
private byte[] signature;
public GetTransactionMessage(byte[] signature) {
this(-1, signature);
super(MessageType.GET_TRANSACTION);
this.dataBytes = Arrays.copyOf(signature, signature.length);
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private GetTransactionMessage(int id, byte[] signature) {
@@ -27,28 +26,12 @@ public class GetTransactionMessage extends Message {
return this.signature;
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
if (bytes.remaining() != TRANSACTION_SIGNATURE_LENGTH)
return null;
byte[] signature = new byte[TRANSACTION_SIGNATURE_LENGTH];
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
bytes.get(signature);
return new GetTransactionMessage(id, signature);
}
@Override
protected byte[] toData() {
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(this.signature);
return bytes.toByteArray();
} catch (IOException e) {
return null;
}
}
}

View File

@@ -1,25 +1,21 @@
package org.qortal.network.message;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
public class GetUnconfirmedTransactionsMessage extends Message {
public GetUnconfirmedTransactionsMessage() {
this(-1);
super(MessageType.GET_UNCONFIRMED_TRANSACTIONS);
this.dataBytes = EMPTY_DATA_BYTES;
}
private GetUnconfirmedTransactionsMessage(int id) {
super(id, MessageType.GET_UNCONFIRMED_TRANSACTIONS);
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
return new GetUnconfirmedTransactionsMessage(id);
}
@Override
protected byte[] toData() {
return new byte[0];
}
}

View File

@@ -3,7 +3,6 @@ package org.qortal.network.message;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toMap;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Map;
@@ -22,7 +21,7 @@ public class GoodbyeMessage extends Message {
private static final Map<Integer, Reason> map = stream(Reason.values())
.collect(toMap(reason -> reason.value, reason -> reason));
private Reason(int value) {
Reason(int value) {
this.value = value;
}
@@ -31,7 +30,14 @@ public class GoodbyeMessage extends Message {
}
}
private final Reason reason;
private Reason reason;
public GoodbyeMessage(Reason reason) {
super(MessageType.GOODBYE);
this.dataBytes = Ints.toByteArray(reason.value);
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private GoodbyeMessage(int id, Reason reason) {
super(id, MessageType.GOODBYE);
@@ -39,27 +45,18 @@ public class GoodbyeMessage extends Message {
this.reason = reason;
}
public GoodbyeMessage(Reason reason) {
this(-1, reason);
}
public Reason getReason() {
return this.reason;
}
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) {
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws MessageException {
int reasonValue = byteBuffer.getInt();
Reason reason = Reason.valueOf(reasonValue);
if (reason == null)
return null;
throw new MessageException("Invalid reason " + reasonValue + " in GOODBYE message");
return new GoodbyeMessage(id, reason);
}
@Override
protected byte[] toData() throws IOException {
return Ints.toByteArray(this.reason.value);
}
}

View File

@@ -2,7 +2,6 @@ package org.qortal.network.message;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import org.qortal.transform.Transformer;
@@ -19,7 +18,24 @@ public class HeightV2Message extends Message {
private byte[] minterPublicKey;
public HeightV2Message(int height, byte[] signature, long timestamp, byte[] minterPublicKey) {
this(-1, height, signature, timestamp, minterPublicKey);
super(MessageType.HEIGHT_V2);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try {
bytes.write(Ints.toByteArray(height));
bytes.write(signature);
bytes.write(Longs.toByteArray(timestamp));
bytes.write(minterPublicKey);
} catch (IOException e) {
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
}
this.dataBytes = bytes.toByteArray();
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private HeightV2Message(int id, int height, byte[] signature, long timestamp, byte[] minterPublicKey) {
@@ -47,7 +63,7 @@ public class HeightV2Message extends Message {
return this.minterPublicKey;
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
int height = bytes.getInt();
byte[] signature = new byte[BlockTransformer.BLOCK_SIGNATURE_LENGTH];
@@ -61,23 +77,4 @@ public class HeightV2Message extends Message {
return new HeightV2Message(id, height, signature, timestamp, minterPublicKey);
}
@Override
protected byte[] toData() {
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(Ints.toByteArray(this.height));
bytes.write(this.signature);
bytes.write(Longs.toByteArray(this.timestamp));
bytes.write(this.minterPublicKey);
return bytes.toByteArray();
} catch (IOException e) {
return null;
}
}
}

View File

@@ -11,9 +11,28 @@ import com.google.common.primitives.Longs;
public class HelloMessage extends Message {
private final long timestamp;
private final String versionString;
private final String senderPeerAddress;
private long timestamp;
private String versionString;
private String senderPeerAddress;
public HelloMessage(long timestamp, String versionString, String senderPeerAddress) {
super(MessageType.HELLO);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try {
bytes.write(Longs.toByteArray(timestamp));
Serialization.serializeSizedString(bytes, versionString);
Serialization.serializeSizedString(bytes, senderPeerAddress);
} catch (IOException e) {
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
}
this.dataBytes = bytes.toByteArray();
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private HelloMessage(int id, long timestamp, String versionString, String senderPeerAddress) {
super(id, MessageType.HELLO);
@@ -23,10 +42,6 @@ public class HelloMessage extends Message {
this.senderPeerAddress = senderPeerAddress;
}
public HelloMessage(long timestamp, String versionString, String senderPeerAddress) {
this(-1, timestamp, versionString, senderPeerAddress);
}
public long getTimestamp() {
return this.timestamp;
}
@@ -39,31 +54,23 @@ public class HelloMessage extends Message {
return this.senderPeerAddress;
}
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws TransformationException {
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws MessageException {
long timestamp = byteBuffer.getLong();
String versionString = Serialization.deserializeSizedString(byteBuffer, 255);
// Sender peer address added in v3.0, so is an optional field. Older versions won't send it.
String versionString;
String senderPeerAddress = null;
if (byteBuffer.hasRemaining()) {
senderPeerAddress = Serialization.deserializeSizedString(byteBuffer, 255);
try {
versionString = Serialization.deserializeSizedString(byteBuffer, 255);
// Sender peer address added in v3.0, so is an optional field. Older versions won't send it.
if (byteBuffer.hasRemaining()) {
senderPeerAddress = Serialization.deserializeSizedString(byteBuffer, 255);
}
} catch (TransformationException e) {
throw new MessageException(e.getMessage(), e);
}
return new HelloMessage(id, timestamp, versionString, senderPeerAddress);
}
@Override
protected byte[] toData() throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(Longs.toByteArray(this.timestamp));
Serialization.serializeSizedString(bytes, this.versionString);
Serialization.serializeSizedString(bytes, this.senderPeerAddress);
return bytes.toByteArray();
}
}

View File

@@ -1,161 +1,67 @@
package org.qortal.network.message;
import java.util.Map;
import org.qortal.crypto.Crypto;
import org.qortal.network.Network;
import org.qortal.transform.TransformationException;
import com.google.common.primitives.Ints;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toMap;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.Arrays;
/**
* Network message for sending over network, or unpacked data received from network.
* <p></p>
* <p>
* For messages received from network, subclass's {@code fromByteBuffer()} method is used
* to construct a subclassed instance. Original bytes from network are not retained.
* Access to deserialized data should be via subclass's getters. Ideally there should be NO setters!
* </p>
* <p></p>
* <p>
* Each subclass's <b>public</b> constructor is for building a message to send <b>only</b>.
* The constructor will serialize into byte form but <b>not</b> store the passed args.
* Serialized bytes are saved into superclass (Message) {@code dataBytes} and, if not empty,
* a checksum is created and saved into {@code checksumBytes}.
* Therefore: <i>do not use subclass's getters after using constructor!</i>
* </p>
* <p></p>
* <p>
* For subclasses where outgoing versions might be usefully cached, they can implement Clonable
* as long if they are safe to use {@link Object#clone()}.
* </p>
*/
public abstract class Message {
// MAGIC(4) + TYPE(4) + HAS-ID(1) + ID?(4) + DATA-SIZE(4) + CHECKSUM?(4) + DATA?(*)
private static final int MAGIC_LENGTH = 4;
private static final int TYPE_LENGTH = 4;
private static final int HAS_ID_LENGTH = 1;
private static final int ID_LENGTH = 4;
private static final int DATA_SIZE_LENGTH = 4;
private static final int CHECKSUM_LENGTH = 4;
private static final int MAX_DATA_SIZE = 10 * 1024 * 1024; // 10MB
@SuppressWarnings("serial")
public static class MessageException extends Exception {
public MessageException() {
}
protected static final byte[] EMPTY_DATA_BYTES = new byte[0];
public MessageException(String message) {
super(message);
}
protected int id;
protected final MessageType type;
public MessageException(String message, Throwable cause) {
super(message, cause);
}
public MessageException(Throwable cause) {
super(cause);
}
}
public enum MessageType {
// Handshaking
HELLO(0),
GOODBYE(1),
CHALLENGE(2),
RESPONSE(3),
// Status / notifications
HEIGHT_V2(10),
PING(11),
PONG(12),
// Requesting data
PEERS_V2(20),
GET_PEERS(21),
TRANSACTION(30),
GET_TRANSACTION(31),
TRANSACTION_SIGNATURES(40),
GET_UNCONFIRMED_TRANSACTIONS(41),
BLOCK(50),
GET_BLOCK(51),
SIGNATURES(60),
GET_SIGNATURES_V2(61),
BLOCK_SUMMARIES(70),
GET_BLOCK_SUMMARIES(71),
ONLINE_ACCOUNTS(80),
GET_ONLINE_ACCOUNTS(81),
ONLINE_ACCOUNTS_V2(82),
GET_ONLINE_ACCOUNTS_V2(83),
ARBITRARY_DATA(90),
GET_ARBITRARY_DATA(91),
BLOCKS(100),
GET_BLOCKS(101),
ARBITRARY_DATA_FILE(110),
GET_ARBITRARY_DATA_FILE(111),
ARBITRARY_DATA_FILE_LIST(120),
GET_ARBITRARY_DATA_FILE_LIST(121),
ARBITRARY_SIGNATURES(130),
TRADE_PRESENCES(140),
GET_TRADE_PRESENCES(141),
ARBITRARY_METADATA(150),
GET_ARBITRARY_METADATA(151);
public final int value;
public final Method fromByteBufferMethod;
private static final Map<Integer, MessageType> map = stream(MessageType.values())
.collect(toMap(messageType -> messageType.value, messageType -> messageType));
private MessageType(int value) {
this.value = value;
String[] classNameParts = this.name().toLowerCase().split("_");
for (int i = 0; i < classNameParts.length; ++i)
classNameParts[i] = classNameParts[i].substring(0, 1).toUpperCase().concat(classNameParts[i].substring(1));
String className = String.join("", classNameParts);
Method method;
try {
Class<?> subclass = Class.forName(String.join("", Message.class.getPackage().getName(), ".", className, "Message"));
method = subclass.getDeclaredMethod("fromByteBuffer", int.class, ByteBuffer.class);
} catch (ClassNotFoundException | NoSuchMethodException | SecurityException e) {
method = null;
}
this.fromByteBufferMethod = method;
}
public static MessageType valueOf(int value) {
return map.get(value);
}
public Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws MessageException {
if (this.fromByteBufferMethod == null)
throw new MessageException("Unsupported message type [" + value + "] during conversion from bytes");
try {
return (Message) this.fromByteBufferMethod.invoke(null, id, byteBuffer);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
if (e.getCause() instanceof BufferUnderflowException)
throw new MessageException("Byte data too short for " + name() + " message");
throw new MessageException("Internal error with " + name() + " message during conversion from bytes");
}
}
}
private int id;
private MessageType type;
/** Serialized outgoing message data. Expected to be written to by subclass. */
protected byte[] dataBytes;
/** Serialized outgoing message checksum. Expected to be written to by subclass. */
protected byte[] checksumBytes;
/** Typically called by subclass when constructing message from received network data. */
protected Message(int id, MessageType type) {
this.id = id;
this.type = type;
}
/** Typically called by subclass when constructing outgoing message. */
protected Message(MessageType type) {
this(-1, type);
}
@@ -179,9 +85,9 @@ public abstract class Message {
/**
* Attempt to read a message from byte buffer.
*
* @param readOnlyBuffer
* @param readOnlyBuffer ByteBuffer containing bytes read from network
* @return null if no complete message can be read
* @throws MessageException
* @throws MessageException if message could not be decoded or is invalid
*/
public static Message fromByteBuffer(ByteBuffer readOnlyBuffer) throws MessageException {
try {
@@ -256,9 +162,27 @@ public abstract class Message {
return Arrays.copyOfRange(Crypto.digest(dataBuffer), 0, CHECKSUM_LENGTH);
}
public void checkValidOutgoing() throws MessageException {
// We expect subclass to have initialized these
if (this.dataBytes == null)
throw new MessageException("Missing data payload");
if (this.dataBytes.length > 0 && this.checksumBytes == null)
throw new MessageException("Missing data checksum");
}
public byte[] toBytes() throws MessageException {
checkValidOutgoing();
// We can calculate exact length
int messageLength = MAGIC_LENGTH + TYPE_LENGTH + HAS_ID_LENGTH;
messageLength += this.hasId() ? ID_LENGTH : 0;
messageLength += DATA_SIZE_LENGTH + this.dataBytes.length > 0 ? CHECKSUM_LENGTH + this.dataBytes.length : 0;
if (messageLength > MAX_DATA_SIZE)
throw new MessageException(String.format("About to send message with length %d larger than allowed %d", messageLength, MAX_DATA_SIZE));
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream(256);
ByteArrayOutputStream bytes = new ByteArrayOutputStream(messageLength);
// Magic
bytes.write(Network.getInstance().getMessageMagic());
@@ -273,26 +197,30 @@ public abstract class Message {
bytes.write(0);
}
byte[] data = this.toData();
if (data == null)
throw new MessageException("Missing data payload");
bytes.write(Ints.toByteArray(this.dataBytes.length));
bytes.write(Ints.toByteArray(data.length));
if (data.length > 0) {
bytes.write(generateChecksum(data));
bytes.write(data);
if (this.dataBytes.length > 0) {
bytes.write(this.checksumBytes);
bytes.write(this.dataBytes);
}
if (bytes.size() > MAX_DATA_SIZE)
throw new MessageException(String.format("About to send message with length %d larger than allowed %d", bytes.size(), MAX_DATA_SIZE));
return bytes.toByteArray();
} catch (IOException | TransformationException e) {
} catch (IOException e) {
throw new MessageException("Failed to serialize message", e);
}
}
protected abstract byte[] toData() throws IOException, TransformationException;
public static <M extends Message> M cloneWithNewId(M message, int newId) {
M clone;
try {
clone = (M) message.clone();
} catch (CloneNotSupportedException e) {
throw new UnsupportedOperationException("Message sub-class not cloneable");
}
clone.setId(newId);
return clone;
}
}

View File

@@ -0,0 +1,19 @@
package org.qortal.network.message;
@SuppressWarnings("serial")
public class MessageException extends Exception {
public MessageException() {
}
public MessageException(String message) {
super(message);
}
public MessageException(String message, Throwable cause) {
super(message, cause);
}
public MessageException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,8 @@
package org.qortal.network.message;
import java.nio.ByteBuffer;
@FunctionalInterface
public interface MessageProducer {
Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws MessageException;
}

View File

@@ -0,0 +1,96 @@
package org.qortal.network.message;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.Map;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toMap;
public enum MessageType {
// Handshaking
HELLO(0, HelloMessage::fromByteBuffer),
GOODBYE(1, GoodbyeMessage::fromByteBuffer),
CHALLENGE(2, ChallengeMessage::fromByteBuffer),
RESPONSE(3, ResponseMessage::fromByteBuffer),
// Status / notifications
HEIGHT_V2(10, HeightV2Message::fromByteBuffer),
PING(11, PingMessage::fromByteBuffer),
PONG(12, PongMessage::fromByteBuffer),
// Requesting data
PEERS_V2(20, PeersV2Message::fromByteBuffer),
GET_PEERS(21, GetPeersMessage::fromByteBuffer),
TRANSACTION(30, TransactionMessage::fromByteBuffer),
GET_TRANSACTION(31, GetTransactionMessage::fromByteBuffer),
TRANSACTION_SIGNATURES(40, TransactionSignaturesMessage::fromByteBuffer),
GET_UNCONFIRMED_TRANSACTIONS(41, GetUnconfirmedTransactionsMessage::fromByteBuffer),
BLOCK(50, BlockMessage::fromByteBuffer),
GET_BLOCK(51, GetBlockMessage::fromByteBuffer),
SIGNATURES(60, SignaturesMessage::fromByteBuffer),
GET_SIGNATURES_V2(61, GetSignaturesV2Message::fromByteBuffer),
BLOCK_SUMMARIES(70, BlockSummariesMessage::fromByteBuffer),
GET_BLOCK_SUMMARIES(71, GetBlockSummariesMessage::fromByteBuffer),
ONLINE_ACCOUNTS(80, OnlineAccountsMessage::fromByteBuffer),
GET_ONLINE_ACCOUNTS(81, GetOnlineAccountsMessage::fromByteBuffer),
ONLINE_ACCOUNTS_V2(82, OnlineAccountsV2Message::fromByteBuffer),
GET_ONLINE_ACCOUNTS_V2(83, GetOnlineAccountsV2Message::fromByteBuffer),
ARBITRARY_DATA(90, ArbitraryDataMessage::fromByteBuffer),
GET_ARBITRARY_DATA(91, GetArbitraryDataMessage::fromByteBuffer),
BLOCKS(100, null), // unsupported
GET_BLOCKS(101, null), // unsupported
ARBITRARY_DATA_FILE(110, ArbitraryDataFileMessage::fromByteBuffer),
GET_ARBITRARY_DATA_FILE(111, GetArbitraryDataFileMessage::fromByteBuffer),
ARBITRARY_DATA_FILE_LIST(120, ArbitraryDataFileListMessage::fromByteBuffer),
GET_ARBITRARY_DATA_FILE_LIST(121, GetArbitraryDataFileListMessage::fromByteBuffer),
ARBITRARY_SIGNATURES(130, ArbitrarySignaturesMessage::fromByteBuffer),
TRADE_PRESENCES(140, TradePresencesMessage::fromByteBuffer),
GET_TRADE_PRESENCES(141, GetTradePresencesMessage::fromByteBuffer),
ARBITRARY_METADATA(150, ArbitraryMetadataMessage::fromByteBuffer),
GET_ARBITRARY_METADATA(151, GetArbitraryMetadataMessage::fromByteBuffer);
public final int value;
public final MessageProducer fromByteBufferMethod;
private static final Map<Integer, MessageType> map = stream(MessageType.values())
.collect(toMap(messageType -> messageType.value, messageType -> messageType));
MessageType(int value, MessageProducer fromByteBufferMethod) {
this.value = value;
this.fromByteBufferMethod = fromByteBufferMethod;
}
public static MessageType valueOf(int value) {
return map.get(value);
}
/**
* Attempt to read a message from byte buffer.
*
* @param id message ID or -1
* @param byteBuffer ByteBuffer source for message
* @return null if no complete message can be read
* @throws MessageException if message could not be decoded or is invalid
* @throws BufferUnderflowException if not enough bytes in buffer to read message
*/
public Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws MessageException {
if (this.fromByteBufferMethod == null)
throw new MessageException("Message type " + this.name() + " unsupported");
return this.fromByteBufferMethod.fromByteBuffer(id, byteBuffer);
}
}

View File

@@ -2,7 +2,6 @@ package org.qortal.network.message;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
@@ -20,7 +19,26 @@ public class OnlineAccountsMessage extends Message {
private List<OnlineAccountData> onlineAccounts;
public OnlineAccountsMessage(List<OnlineAccountData> onlineAccounts) {
this(-1, onlineAccounts);
super(MessageType.ONLINE_ACCOUNTS);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try {
bytes.write(Ints.toByteArray(onlineAccounts.size()));
for (OnlineAccountData onlineAccountData : onlineAccounts) {
bytes.write(Longs.toByteArray(onlineAccountData.getTimestamp()));
bytes.write(onlineAccountData.getSignature());
bytes.write(onlineAccountData.getPublicKey());
}
} catch (IOException e) {
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
}
this.dataBytes = bytes.toByteArray();
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private OnlineAccountsMessage(int id, List<OnlineAccountData> onlineAccounts) {
@@ -33,7 +51,7 @@ public class OnlineAccountsMessage extends Message {
return this.onlineAccounts;
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
final int accountCount = bytes.getInt();
List<OnlineAccountData> onlineAccounts = new ArrayList<>(accountCount);
@@ -54,27 +72,4 @@ public class OnlineAccountsMessage extends Message {
return new OnlineAccountsMessage(id, onlineAccounts);
}
@Override
protected byte[] toData() {
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(Ints.toByteArray(this.onlineAccounts.size()));
for (int i = 0; i < this.onlineAccounts.size(); ++i) {
OnlineAccountData onlineAccountData = this.onlineAccounts.get(i);
bytes.write(Longs.toByteArray(onlineAccountData.getTimestamp()));
bytes.write(onlineAccountData.getSignature());
bytes.write(onlineAccountData.getPublicKey());
}
return bytes.toByteArray();
} catch (IOException e) {
return null;
}
}
}

View File

@@ -7,13 +7,11 @@ import org.qortal.transform.Transformer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* For sending online accounts info to remote peer.
@@ -25,11 +23,52 @@ import java.util.stream.Collectors;
* Also V2 only builds online accounts message once!
*/
public class OnlineAccountsV2Message extends Message {
private List<OnlineAccountData> onlineAccounts;
private byte[] cachedData;
public OnlineAccountsV2Message(List<OnlineAccountData> onlineAccounts) {
this(-1, onlineAccounts);
super(MessageType.ONLINE_ACCOUNTS_V2);
// Shortcut in case we have no online accounts
if (onlineAccounts.isEmpty()) {
this.dataBytes = Ints.toByteArray(0);
this.checksumBytes = Message.generateChecksum(this.dataBytes);
return;
}
// How many of each timestamp
Map<Long, Integer> countByTimestamp = new HashMap<>();
for (OnlineAccountData onlineAccountData : onlineAccounts) {
Long timestamp = onlineAccountData.getTimestamp();
countByTimestamp.compute(timestamp, (k, v) -> v == null ? 1 : ++v);
}
// We should know exactly how many bytes to allocate now
int byteSize = countByTimestamp.size() * (Transformer.INT_LENGTH + Transformer.TIMESTAMP_LENGTH)
+ onlineAccounts.size() * (Transformer.SIGNATURE_LENGTH + Transformer.PUBLIC_KEY_LENGTH);
ByteArrayOutputStream bytes = new ByteArrayOutputStream(byteSize);
try {
for (long timestamp : countByTimestamp.keySet()) {
bytes.write(Ints.toByteArray(countByTimestamp.get(timestamp)));
bytes.write(Longs.toByteArray(timestamp));
for (OnlineAccountData onlineAccountData : onlineAccounts) {
if (onlineAccountData.getTimestamp() == timestamp) {
bytes.write(onlineAccountData.getSignature());
bytes.write(onlineAccountData.getPublicKey());
}
}
}
} catch (IOException e) {
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
}
this.dataBytes = bytes.toByteArray();
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private OnlineAccountsV2Message(int id, List<OnlineAccountData> onlineAccounts) {
@@ -42,7 +81,7 @@ public class OnlineAccountsV2Message extends Message {
return this.onlineAccounts;
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws MessageException {
int accountCount = bytes.getInt();
List<OnlineAccountData> onlineAccounts = new ArrayList<>(accountCount);
@@ -71,54 +110,4 @@ public class OnlineAccountsV2Message extends Message {
return new OnlineAccountsV2Message(id, onlineAccounts);
}
@Override
protected synchronized byte[] toData() {
if (this.cachedData != null)
return this.cachedData;
// Shortcut in case we have no online accounts
if (this.onlineAccounts.isEmpty()) {
this.cachedData = Ints.toByteArray(0);
return this.cachedData;
}
// How many of each timestamp
Map<Long, Integer> countByTimestamp = new HashMap<>();
for (int i = 0; i < this.onlineAccounts.size(); ++i) {
OnlineAccountData onlineAccountData = this.onlineAccounts.get(i);
Long timestamp = onlineAccountData.getTimestamp();
countByTimestamp.compute(timestamp, (k, v) -> v == null ? 1 : ++v);
}
// We should know exactly how many bytes to allocate now
int byteSize = countByTimestamp.size() * (Transformer.INT_LENGTH + Transformer.TIMESTAMP_LENGTH)
+ this.onlineAccounts.size() * (Transformer.SIGNATURE_LENGTH + Transformer.PUBLIC_KEY_LENGTH);
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream(byteSize);
for (long timestamp : countByTimestamp.keySet()) {
bytes.write(Ints.toByteArray(countByTimestamp.get(timestamp)));
bytes.write(Longs.toByteArray(timestamp));
for (int i = 0; i < this.onlineAccounts.size(); ++i) {
OnlineAccountData onlineAccountData = this.onlineAccounts.get(i);
if (onlineAccountData.getTimestamp() == timestamp) {
bytes.write(onlineAccountData.getSignature());
bytes.write(onlineAccountData.getPublicKey());
}
}
}
this.cachedData = bytes.toByteArray();
return this.cachedData;
} catch (IOException e) {
return null;
}
}
}

View File

@@ -2,7 +2,6 @@ package org.qortal.network.message;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
@@ -19,7 +18,35 @@ public class PeersV2Message extends Message {
private List<PeerAddress> peerAddresses;
public PeersV2Message(List<PeerAddress> peerAddresses) {
this(-1, peerAddresses);
super(MessageType.PEERS_V2);
List<byte[]> addresses = new ArrayList<>();
// First entry represents sending node but contains only port number with empty address.
addresses.add(("0.0.0.0:" + Settings.getInstance().getListenPort()).getBytes(StandardCharsets.UTF_8));
for (PeerAddress peerAddress : peerAddresses)
addresses.add(peerAddress.toString().getBytes(StandardCharsets.UTF_8));
// We can't send addresses that are longer than 255 bytes as length itself is encoded in one byte.
addresses.removeIf(addressString -> addressString.length > 255);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try {
// Number of entries
bytes.write(Ints.toByteArray(addresses.size()));
for (byte[] address : addresses) {
bytes.write(address.length);
bytes.write(address);
}
} catch (IOException e) {
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
}
this.dataBytes = bytes.toByteArray();
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private PeersV2Message(int id, List<PeerAddress> peerAddresses) {
@@ -32,7 +59,7 @@ public class PeersV2Message extends Message {
return this.peerAddresses;
}
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws UnsupportedEncodingException {
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws MessageException {
// Read entry count
int count = byteBuffer.getInt();
@@ -49,43 +76,11 @@ public class PeersV2Message extends Message {
PeerAddress peerAddress = PeerAddress.fromString(addressString);
peerAddresses.add(peerAddress);
} catch (IllegalArgumentException e) {
// Not valid - ignore
throw new MessageException("Invalid peer address in received PEERS_V2 message");
}
}
return new PeersV2Message(id, peerAddresses);
}
@Override
protected byte[] toData() {
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
List<byte[]> addresses = new ArrayList<>();
// First entry represents sending node but contains only port number with empty address.
addresses.add(("0.0.0.0:" + Settings.getInstance().getListenPort()).getBytes(StandardCharsets.UTF_8));
for (PeerAddress peerAddress : this.peerAddresses)
addresses.add(peerAddress.toString().getBytes(StandardCharsets.UTF_8));
// We can't send addresses that are longer than 255 bytes as length itself is encoded in one byte.
addresses.removeIf(addressString -> addressString.length > 255);
// Serialize
// Number of entries
bytes.write(Ints.toByteArray(addresses.size()));
for (byte[] address : addresses) {
bytes.write(address.length);
bytes.write(address);
}
return bytes.toByteArray();
} catch (IOException e) {
return null;
}
}
}

View File

@@ -1,25 +1,21 @@
package org.qortal.network.message;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
public class PingMessage extends Message {
public PingMessage() {
this(-1);
super(MessageType.PING);
this.dataBytes = EMPTY_DATA_BYTES;
}
private PingMessage(int id) {
super(id, MessageType.PING);
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
return new PingMessage(id);
}
@Override
protected byte[] toData() {
return new byte[0];
}
}

View File

@@ -0,0 +1,21 @@
package org.qortal.network.message;
import java.nio.ByteBuffer;
public class PongMessage extends Message {
public PongMessage() {
super(MessageType.PONG);
this.dataBytes = EMPTY_DATA_BYTES;
}
private PongMessage(int id) {
super(id, MessageType.PONG);
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
return new PongMessage(id);
}
}

View File

@@ -10,8 +10,25 @@ public class ResponseMessage extends Message {
public static final int DATA_LENGTH = 32;
private final int nonce;
private final byte[] data;
private int nonce;
private byte[] data;
public ResponseMessage(int nonce, byte[] data) {
super(MessageType.RESPONSE);
ByteArrayOutputStream bytes = new ByteArrayOutputStream(4 + DATA_LENGTH);
try {
bytes.write(Ints.toByteArray(nonce));
bytes.write(data);
} catch (IOException e) {
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
}
this.dataBytes = bytes.toByteArray();
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private ResponseMessage(int id, int nonce, byte[] data) {
super(id, MessageType.RESPONSE);
@@ -20,10 +37,6 @@ public class ResponseMessage extends Message {
this.data = data;
}
public ResponseMessage(int nonce, byte[] data) {
this(-1, nonce, data);
}
public int getNonce() {
return this.nonce;
}
@@ -41,15 +54,4 @@ public class ResponseMessage extends Message {
return new ResponseMessage(id, nonce, data);
}
@Override
protected byte[] toData() throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream(4 + DATA_LENGTH);
bytes.write(Ints.toByteArray(this.nonce));
bytes.write(data);
return bytes.toByteArray();
}
}

View File

@@ -2,7 +2,7 @@ package org.qortal.network.message;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
@@ -13,12 +13,24 @@ import com.google.common.primitives.Ints;
public class SignaturesMessage extends Message {
private static final int BLOCK_SIGNATURE_LENGTH = BlockTransformer.BLOCK_SIGNATURE_LENGTH;
private List<byte[]> signatures;
public SignaturesMessage(List<byte[]> signatures) {
this(-1, signatures);
super(MessageType.SIGNATURES);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try {
bytes.write(Ints.toByteArray(signatures.size()));
for (byte[] signature : signatures)
bytes.write(signature);
} catch (IOException e) {
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
}
this.dataBytes = bytes.toByteArray();
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private SignaturesMessage(int id, List<byte[]> signatures) {
@@ -31,15 +43,15 @@ public class SignaturesMessage extends Message {
return this.signatures;
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
int count = bytes.getInt();
if (bytes.remaining() != count * BLOCK_SIGNATURE_LENGTH)
return null;
if (bytes.remaining() < count * BlockTransformer.BLOCK_SIGNATURE_LENGTH)
throw new BufferUnderflowException();
List<byte[]> signatures = new ArrayList<>();
for (int i = 0; i < count; ++i) {
byte[] signature = new byte[BLOCK_SIGNATURE_LENGTH];
byte[] signature = new byte[BlockTransformer.BLOCK_SIGNATURE_LENGTH];
bytes.get(signature);
signatures.add(signature);
}
@@ -47,20 +59,4 @@ public class SignaturesMessage extends Message {
return new SignaturesMessage(id, signatures);
}
@Override
protected byte[] toData() {
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(Ints.toByteArray(this.signatures.size()));
for (byte[] signature : this.signatures)
bytes.write(signature);
return bytes.toByteArray();
} catch (IOException e) {
return null;
}
}
}

View File

@@ -8,7 +8,6 @@ import org.qortal.utils.Base58;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
@@ -21,11 +20,55 @@ import java.util.Map;
* Groups of: number of entries, timestamp, then pubkey + sig + AT address for each entry.
*/
public class TradePresencesMessage extends Message {
private List<TradePresenceData> tradePresences;
private byte[] cachedData;
public TradePresencesMessage(List<TradePresenceData> tradePresences) {
this(-1, tradePresences);
super(MessageType.TRADE_PRESENCES);
// Shortcut in case we have no trade presences
if (tradePresences.isEmpty()) {
this.dataBytes = Ints.toByteArray(0);
this.checksumBytes = Message.generateChecksum(this.dataBytes);
return;
}
// How many of each timestamp
Map<Long, Integer> countByTimestamp = new HashMap<>();
for (TradePresenceData tradePresenceData : tradePresences) {
Long timestamp = tradePresenceData.getTimestamp();
countByTimestamp.compute(timestamp, (k, v) -> v == null ? 1 : ++v);
}
// We should know exactly how many bytes to allocate now
int byteSize = countByTimestamp.size() * (Transformer.INT_LENGTH + Transformer.TIMESTAMP_LENGTH)
+ tradePresences.size() * (Transformer.PUBLIC_KEY_LENGTH + Transformer.SIGNATURE_LENGTH + Transformer.ADDRESS_LENGTH);
ByteArrayOutputStream bytes = new ByteArrayOutputStream(byteSize);
try {
for (long timestamp : countByTimestamp.keySet()) {
bytes.write(Ints.toByteArray(countByTimestamp.get(timestamp)));
bytes.write(Longs.toByteArray(timestamp));
for (TradePresenceData tradePresenceData : tradePresences) {
if (tradePresenceData.getTimestamp() == timestamp) {
bytes.write(tradePresenceData.getPublicKey());
bytes.write(tradePresenceData.getSignature());
bytes.write(Base58.decode(tradePresenceData.getAtAddress()));
}
}
}
} catch (IOException e) {
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
}
this.dataBytes = bytes.toByteArray();
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private TradePresencesMessage(int id, List<TradePresenceData> tradePresences) {
@@ -38,7 +81,7 @@ public class TradePresencesMessage extends Message {
return this.tradePresences;
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
int groupedEntriesCount = bytes.getInt();
List<TradePresenceData> tradePresences = new ArrayList<>(groupedEntriesCount);
@@ -71,53 +114,4 @@ public class TradePresencesMessage extends Message {
return new TradePresencesMessage(id, tradePresences);
}
@Override
protected synchronized byte[] toData() {
if (this.cachedData != null)
return this.cachedData;
// Shortcut in case we have no trade presences
if (this.tradePresences.isEmpty()) {
this.cachedData = Ints.toByteArray(0);
return this.cachedData;
}
// How many of each timestamp
Map<Long, Integer> countByTimestamp = new HashMap<>();
for (TradePresenceData tradePresenceData : this.tradePresences) {
Long timestamp = tradePresenceData.getTimestamp();
countByTimestamp.compute(timestamp, (k, v) -> v == null ? 1 : ++v);
}
// We should know exactly how many bytes to allocate now
int byteSize = countByTimestamp.size() * (Transformer.INT_LENGTH + Transformer.TIMESTAMP_LENGTH)
+ this.tradePresences.size() * (Transformer.PUBLIC_KEY_LENGTH + Transformer.SIGNATURE_LENGTH + Transformer.ADDRESS_LENGTH);
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream(byteSize);
for (long timestamp : countByTimestamp.keySet()) {
bytes.write(Ints.toByteArray(countByTimestamp.get(timestamp)));
bytes.write(Longs.toByteArray(timestamp));
for (TradePresenceData tradePresenceData : this.tradePresences) {
if (tradePresenceData.getTimestamp() == timestamp) {
bytes.write(tradePresenceData.getPublicKey());
bytes.write(tradePresenceData.getSignature());
bytes.write(Base58.decode(tradePresenceData.getAtAddress()));
}
}
}
this.cachedData = bytes.toByteArray();
return this.cachedData;
} catch (IOException e) {
return null;
}
}
}

View File

@@ -1,6 +1,5 @@
package org.qortal.network.message;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import org.qortal.data.transaction.TransactionData;
@@ -11,8 +10,11 @@ public class TransactionMessage extends Message {
private TransactionData transactionData;
public TransactionMessage(TransactionData transactionData) {
this(-1, transactionData);
public TransactionMessage(TransactionData transactionData) throws TransformationException {
super(MessageType.TRANSACTION);
this.dataBytes = TransactionTransformer.toBytes(transactionData);
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private TransactionMessage(int id, TransactionData transactionData) {
@@ -25,26 +27,16 @@ public class TransactionMessage extends Message {
return this.transactionData;
}
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws UnsupportedEncodingException {
try {
TransactionData transactionData = TransactionTransformer.fromByteBuffer(byteBuffer);
return new TransactionMessage(id, transactionData);
} catch (TransformationException e) {
return null;
}
}
@Override
protected byte[] toData() {
if (this.transactionData == null)
return null;
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws MessageException {
TransactionData transactionData;
try {
return TransactionTransformer.toBytes(this.transactionData);
transactionData = TransactionTransformer.fromByteBuffer(byteBuffer);
} catch (TransformationException e) {
return null;
throw new MessageException(e.getMessage(), e);
}
return new TransactionMessage(id, transactionData);
}
}

View File

@@ -2,7 +2,7 @@ package org.qortal.network.message;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
@@ -13,12 +13,24 @@ import com.google.common.primitives.Ints;
public class TransactionSignaturesMessage extends Message {
private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
private List<byte[]> signatures;
public TransactionSignaturesMessage(List<byte[]> signatures) {
this(-1, signatures);
super(MessageType.TRANSACTION_SIGNATURES);
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try {
bytes.write(Ints.toByteArray(signatures.size()));
for (byte[] signature : signatures)
bytes.write(signature);
} catch (IOException e) {
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
}
this.dataBytes = bytes.toByteArray();
this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private TransactionSignaturesMessage(int id, List<byte[]> signatures) {
@@ -31,15 +43,15 @@ public class TransactionSignaturesMessage extends Message {
return this.signatures;
}
public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
public static Message fromByteBuffer(int id, ByteBuffer bytes) {
int count = bytes.getInt();
if (bytes.remaining() != count * SIGNATURE_LENGTH)
return null;
if (bytes.remaining() < count * Transformer.SIGNATURE_LENGTH)
throw new BufferUnderflowException();
List<byte[]> signatures = new ArrayList<>();
for (int i = 0; i < count; ++i) {
byte[] signature = new byte[SIGNATURE_LENGTH];
byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
bytes.get(signature);
signatures.add(signature);
}
@@ -47,20 +59,4 @@ public class TransactionSignaturesMessage extends Message {
return new TransactionSignaturesMessage(id, signatures);
}
@Override
protected byte[] toData() {
try {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
bytes.write(Ints.toByteArray(this.signatures.size()));
for (byte[] signature : this.signatures)
bytes.write(signature);
return bytes.toByteArray();
} catch (IOException e) {
return null;
}
}
}

View File

@@ -0,0 +1,22 @@
package org.qortal.network.task;
import org.qortal.controller.Controller;
import org.qortal.network.Network;
import org.qortal.network.Peer;
import org.qortal.network.message.Message;
import org.qortal.utils.ExecuteProduceConsume.Task;
public class BroadcastTask implements Task {
public BroadcastTask() {
}
@Override
public String getName() {
return "BroadcastTask";
}
@Override
public void perform() throws InterruptedException {
Controller.getInstance().doNetworkBroadcast();
}
}

View File

@@ -0,0 +1,97 @@
package org.qortal.network.task;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.network.Network;
import org.qortal.network.Peer;
import org.qortal.network.PeerAddress;
import org.qortal.settings.Settings;
import org.qortal.utils.ExecuteProduceConsume.Task;
import org.qortal.utils.NTP;
import java.io.IOException;
import java.nio.channels.SelectionKey;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.List;
public class ChannelAcceptTask implements Task {
private static final Logger LOGGER = LogManager.getLogger(ChannelAcceptTask.class);
private final ServerSocketChannel serverSocketChannel;
public ChannelAcceptTask(ServerSocketChannel serverSocketChannel) {
this.serverSocketChannel = serverSocketChannel;
}
@Override
public String getName() {
return "ChannelAcceptTask";
}
@Override
public void perform() throws InterruptedException {
Network network = Network.getInstance();
SocketChannel socketChannel;
try {
if (network.getImmutableConnectedPeers().size() >= network.getMaxPeers()) {
// We have enough peers
LOGGER.debug("Ignoring pending incoming connections because the server is full");
return;
}
socketChannel = serverSocketChannel.accept();
network.setInterestOps(serverSocketChannel, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
return;
}
// No connection actually accepted?
if (socketChannel == null) {
return;
}
PeerAddress address = PeerAddress.fromSocket(socketChannel.socket());
List<String> fixedNetwork = Settings.getInstance().getFixedNetwork();
if (fixedNetwork != null && !fixedNetwork.isEmpty() && network.ipNotInFixedList(address, fixedNetwork)) {
try {
LOGGER.debug("Connection discarded from peer {} as not in the fixed network list", address);
socketChannel.close();
} catch (IOException e) {
// IGNORE
}
return;
}
final Long now = NTP.getTime();
Peer newPeer;
try {
if (now == null) {
LOGGER.debug("Connection discarded from peer {} due to lack of NTP sync", address);
socketChannel.close();
return;
}
LOGGER.debug("Connection accepted from peer {}", address);
newPeer = new Peer(socketChannel);
network.addConnectedPeer(newPeer);
} catch (IOException e) {
if (socketChannel.isOpen()) {
try {
LOGGER.debug("Connection failed from peer {} while connecting/closing", address);
socketChannel.close();
} catch (IOException ce) {
// Couldn't close?
}
}
return;
}
network.onPeerReady(newPeer);
}
}

View File

@@ -0,0 +1,49 @@
package org.qortal.network.task;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.network.Network;
import org.qortal.network.Peer;
import org.qortal.utils.ExecuteProduceConsume.Task;
import java.io.IOException;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
public class ChannelReadTask implements Task {
private static final Logger LOGGER = LogManager.getLogger(ChannelReadTask.class);
private final SocketChannel socketChannel;
private final Peer peer;
private final String name;
public ChannelReadTask(SocketChannel socketChannel, Peer peer) {
this.socketChannel = socketChannel;
this.peer = peer;
this.name = "ChannelReadTask::" + peer;
}
@Override
public String getName() {
return name;
}
@Override
public void perform() throws InterruptedException {
try {
peer.readChannel();
Network.getInstance().setInterestOps(socketChannel, SelectionKey.OP_READ);
} catch (IOException e) {
if (e.getMessage() != null && e.getMessage().toLowerCase().contains("connection reset")) {
peer.disconnect("Connection reset");
return;
}
LOGGER.trace("[{}] Network thread {} encountered I/O error: {}", peer.getPeerConnectionId(),
Thread.currentThread().getId(), e.getMessage(), e);
peer.disconnect("I/O error");
}
}
}

View File

@@ -0,0 +1,52 @@
package org.qortal.network.task;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.network.Network;
import org.qortal.network.Peer;
import org.qortal.utils.ExecuteProduceConsume.Task;
import java.io.IOException;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
public class ChannelWriteTask implements Task {
private static final Logger LOGGER = LogManager.getLogger(ChannelWriteTask.class);
private final SocketChannel socketChannel;
private final Peer peer;
private final String name;
public ChannelWriteTask(SocketChannel socketChannel, Peer peer) {
this.socketChannel = socketChannel;
this.peer = peer;
this.name = "ChannelWriteTask::" + peer;
}
@Override
public String getName() {
return name;
}
@Override
public void perform() throws InterruptedException {
try {
boolean isSocketClogged = peer.writeChannel();
// Tell Network that we've finished
Network.getInstance().notifyChannelNotWriting(socketChannel);
if (isSocketClogged)
Network.getInstance().setInterestOps(this.socketChannel, SelectionKey.OP_WRITE);
} catch (IOException e) {
if (e.getMessage() != null && e.getMessage().toLowerCase().contains("connection reset")) {
peer.disconnect("Connection reset");
return;
}
LOGGER.trace("[{}] Network thread {} encountered I/O error: {}", peer.getPeerConnectionId(),
Thread.currentThread().getId(), e.getMessage(), e);
peer.disconnect("I/O error");
}
}
}

View File

@@ -0,0 +1,28 @@
package org.qortal.network.task;
import org.qortal.network.Network;
import org.qortal.network.Peer;
import org.qortal.network.message.Message;
import org.qortal.utils.ExecuteProduceConsume.Task;
public class MessageTask implements Task {
private final Peer peer;
private final Message nextMessage;
private final String name;
public MessageTask(Peer peer, Message nextMessage) {
this.peer = peer;
this.nextMessage = nextMessage;
this.name = "MessageTask::" + peer + "::" + nextMessage.getType();
}
@Override
public String getName() {
return name;
}
@Override
public void perform() throws InterruptedException {
Network.getInstance().onMessage(peer, nextMessage);
}
}

View File

@@ -0,0 +1,33 @@
package org.qortal.network.task;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.network.Network;
import org.qortal.network.Peer;
import org.qortal.network.message.Message;
import org.qortal.network.message.MessageType;
import org.qortal.network.message.PingMessage;
import org.qortal.utils.ExecuteProduceConsume.Task;
import org.qortal.utils.NTP;
public class PeerConnectTask implements Task {
private static final Logger LOGGER = LogManager.getLogger(PeerConnectTask.class);
private final Peer peer;
private final String name;
public PeerConnectTask(Peer peer) {
this.peer = peer;
this.name = "PeerConnectTask::" + peer;
}
@Override
public String getName() {
return name;
}
@Override
public void perform() throws InterruptedException {
Network.getInstance().connectPeer(peer);
}
}

View File

@@ -0,0 +1,44 @@
package org.qortal.network.task;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qortal.network.Peer;
import org.qortal.network.message.Message;
import org.qortal.network.message.MessageType;
import org.qortal.network.message.PingMessage;
import org.qortal.utils.ExecuteProduceConsume.Task;
import org.qortal.utils.NTP;
public class PingTask implements Task {
private static final Logger LOGGER = LogManager.getLogger(PingTask.class);
private final Peer peer;
private final Long now;
private final String name;
public PingTask(Peer peer, Long now) {
this.peer = peer;
this.now = now;
this.name = "PingTask::" + peer;
}
@Override
public String getName() {
return name;
}
@Override
public void perform() throws InterruptedException {
PingMessage pingMessage = new PingMessage();
Message message = peer.getResponse(pingMessage);
if (message == null || message.getType() != MessageType.PING) {
LOGGER.debug("[{}] Didn't receive reply from {} for PING ID {}",
peer.getPeerConnectionId(), peer, pingMessage.getId());
peer.disconnect("no ping received");
return;
}
peer.setLastPing(NTP.getTime() - now);
}
}

View File

@@ -26,6 +26,8 @@ import org.qortal.controller.arbitrary.ArbitraryDataStorageManager.*;
import org.qortal.crosschain.Bitcoin.BitcoinNet;
import org.qortal.crosschain.Litecoin.LitecoinNet;
import org.qortal.crosschain.Dogecoin.DogecoinNet;
import org.qortal.crosschain.Digibyte.DigibyteNet;
import org.qortal.crosschain.Ravencoin.RavencoinNet;
import org.qortal.utils.EnumUtils;
// All properties to be converted to JSON via JAXB
@@ -222,6 +224,8 @@ public class Settings {
private BitcoinNet bitcoinNet = BitcoinNet.MAIN;
private LitecoinNet litecoinNet = LitecoinNet.MAIN;
private DogecoinNet dogecoinNet = DogecoinNet.MAIN;
private DigibyteNet digibyteNet = DigibyteNet.MAIN;
private RavencoinNet ravencoinNet = RavencoinNet.MAIN;
// Also crosschain-related:
/** Whether to show SysTray pop-up notifications when trade-bot entries change state */
private boolean tradebotSystrayEnabled = false;
@@ -680,6 +684,14 @@ public class Settings {
return this.dogecoinNet;
}
public DigibyteNet getDigibyteNet() {
return this.digibyteNet;
}
public RavencoinNet getRavencoinNet() {
return this.ravencoinNet;
}
public boolean isTradebotSystrayEnabled() {
return this.tradebotSystrayEnabled;
}

View File

@@ -114,7 +114,7 @@ public class BuyNameTransaction extends Transaction {
public void process() throws DataException {
// Buy Name
Name name = new Name(this.repository, this.buyNameTransactionData.getName());
name.buy(this.buyNameTransactionData);
name.buy(this.buyNameTransactionData, true);
// Save transaction with updated "name reference" pointing to previous transaction that changed name
this.repository.getTransactionRepository().save(this.buyNameTransactionData);

View File

@@ -28,7 +28,6 @@ public abstract class ExecuteProduceConsume implements Runnable {
private final String className;
private final Logger logger;
private final boolean isLoggerTraceEnabled;
protected ExecutorService executor;
@@ -43,12 +42,12 @@ public abstract class ExecuteProduceConsume implements Runnable {
private volatile int tasksConsumed = 0;
private volatile int spawnFailures = 0;
/** Whether a new thread has already been spawned and is waiting to start. Used to prevent spawning multiple new threads. */
private volatile boolean hasThreadPending = false;
public ExecuteProduceConsume(ExecutorService executor) {
this.className = this.getClass().getSimpleName();
this.logger = LogManager.getLogger(this.getClass());
this.isLoggerTraceEnabled = this.logger.isTraceEnabled();
this.executor = executor;
}
@@ -98,15 +97,14 @@ public abstract class ExecuteProduceConsume implements Runnable {
*/
protected abstract Task produceTask(boolean canBlock) throws InterruptedException;
@FunctionalInterface
public interface Task {
public abstract void perform() throws InterruptedException;
String getName();
void perform() throws InterruptedException;
}
@Override
public void run() {
if (this.isLoggerTraceEnabled)
Thread.currentThread().setName(this.className + "-" + Thread.currentThread().getId());
Thread.currentThread().setName(this.className + "-" + Thread.currentThread().getId());
boolean wasThreadPending;
synchronized (this) {
@@ -114,25 +112,19 @@ public abstract class ExecuteProduceConsume implements Runnable {
if (this.activeThreadCount > this.greatestActiveThreadCount)
this.greatestActiveThreadCount = this.activeThreadCount;
if (this.isLoggerTraceEnabled) {
this.logger.trace(() -> String.format("[%d] started, hasThreadPending was: %b, activeThreadCount now: %d",
Thread.currentThread().getId(), this.hasThreadPending, this.activeThreadCount));
}
this.logger.trace(() -> String.format("[%d] started, hasThreadPending was: %b, activeThreadCount now: %d",
Thread.currentThread().getId(), this.hasThreadPending, this.activeThreadCount));
// Defer clearing hasThreadPending to prevent unnecessary threads waiting to produce...
wasThreadPending = this.hasThreadPending;
}
try {
// It's possible this might need to become a class instance private volatile
boolean canBlock = false;
while (!Thread.currentThread().isInterrupted()) {
Task task = null;
String taskType;
if (this.isLoggerTraceEnabled) {
this.logger.trace(() -> String.format("[%d] waiting to produce...", Thread.currentThread().getId()));
}
this.logger.trace(() -> String.format("[%d] waiting to produce...", Thread.currentThread().getId()));
synchronized (this) {
if (wasThreadPending) {
@@ -141,13 +133,13 @@ public abstract class ExecuteProduceConsume implements Runnable {
wasThreadPending = false;
}
final boolean lambdaCanIdle = canBlock;
if (this.isLoggerTraceEnabled) {
this.logger.trace(() -> String.format("[%d] producing, activeThreadCount: %d, consumerCount: %d, canBlock is %b...",
Thread.currentThread().getId(), this.activeThreadCount, this.consumerCount, lambdaCanIdle));
}
// If we're the only non-consuming thread - producer can afford to block this round
boolean canBlock = this.activeThreadCount - this.consumerCount <= 1;
final long beforeProduce = isLoggerTraceEnabled ? System.currentTimeMillis() : 0;
this.logger.trace(() -> String.format("[%d] producing... [activeThreadCount: %d, consumerCount: %d, canBlock: %b]",
Thread.currentThread().getId(), this.activeThreadCount, this.consumerCount, canBlock));
final long beforeProduce = this.logger.isDebugEnabled() ? System.currentTimeMillis() : 0;
try {
task = produceTask(canBlock);
@@ -158,31 +150,36 @@ public abstract class ExecuteProduceConsume implements Runnable {
this.logger.warn(() -> String.format("[%d] exception while trying to produce task", Thread.currentThread().getId()), e);
}
if (this.isLoggerTraceEnabled) {
this.logger.trace(() -> String.format("[%d] producing took %dms", Thread.currentThread().getId(), System.currentTimeMillis() - beforeProduce));
if (this.logger.isDebugEnabled()) {
final long productionPeriod = System.currentTimeMillis() - beforeProduce;
taskType = task == null ? "no task" : task.getName();
this.logger.debug(() -> String.format("[%d] produced [%s] in %dms [canBlock: %b]",
Thread.currentThread().getId(),
taskType,
productionPeriod,
canBlock
));
} else {
taskType = null;
}
}
if (task == null)
synchronized (this) {
if (this.isLoggerTraceEnabled) {
this.logger.trace(() -> String.format("[%d] no task, activeThreadCount: %d, consumerCount: %d",
Thread.currentThread().getId(), this.activeThreadCount, this.consumerCount));
}
this.logger.trace(() -> String.format("[%d] no task, activeThreadCount: %d, consumerCount: %d",
Thread.currentThread().getId(), this.activeThreadCount, this.consumerCount));
if (this.activeThreadCount > this.consumerCount + 1) {
// If we have an excess of non-consuming threads then we can exit
if (this.activeThreadCount - this.consumerCount > 1) {
--this.activeThreadCount;
if (this.isLoggerTraceEnabled) {
this.logger.trace(() -> String.format("[%d] ending, activeThreadCount now: %d",
Thread.currentThread().getId(), this.activeThreadCount));
}
this.logger.trace(() -> String.format("[%d] ending, activeThreadCount now: %d",
Thread.currentThread().getId(), this.activeThreadCount));
return;
}
// We're the last surviving thread - producer can afford to block next round
canBlock = true;
continue;
}
@@ -192,16 +189,13 @@ public abstract class ExecuteProduceConsume implements Runnable {
++this.tasksProduced;
++this.consumerCount;
if (this.isLoggerTraceEnabled) {
this.logger.trace(() -> String.format("[%d] hasThreadPending: %b, activeThreadCount: %d, consumerCount now: %d",
Thread.currentThread().getId(), this.hasThreadPending, this.activeThreadCount, this.consumerCount));
}
this.logger.trace(() -> String.format("[%d] hasThreadPending: %b, activeThreadCount: %d, consumerCount now: %d",
Thread.currentThread().getId(), this.hasThreadPending, this.activeThreadCount, this.consumerCount));
// If we have no thread pending and no excess of threads then we should spawn a fresh thread
if (!this.hasThreadPending && this.activeThreadCount <= this.consumerCount + 1) {
if (this.isLoggerTraceEnabled) {
this.logger.trace(() -> String.format("[%d] spawning another thread", Thread.currentThread().getId()));
}
if (!this.hasThreadPending && this.activeThreadCount == this.consumerCount) {
this.logger.trace(() -> String.format("[%d] spawning another thread", Thread.currentThread().getId()));
this.hasThreadPending = true;
try {
@@ -209,21 +203,19 @@ public abstract class ExecuteProduceConsume implements Runnable {
} catch (RejectedExecutionException e) {
++this.spawnFailures;
this.hasThreadPending = false;
if (this.isLoggerTraceEnabled) {
this.logger.trace(() -> String.format("[%d] failed to spawn another thread", Thread.currentThread().getId()));
}
this.logger.trace(() -> String.format("[%d] failed to spawn another thread", Thread.currentThread().getId()));
this.onSpawnFailure();
}
} else {
if (this.isLoggerTraceEnabled) {
this.logger.trace(() -> String.format("[%d] NOT spawning another thread", Thread.currentThread().getId()));
}
this.logger.trace(() -> String.format("[%d] NOT spawning another thread", Thread.currentThread().getId()));
}
}
if (this.isLoggerTraceEnabled) {
this.logger.trace(() -> String.format("[%d] performing task...", Thread.currentThread().getId()));
}
this.logger.trace(() -> String.format("[%d] consuming [%s] task...", Thread.currentThread().getId(), taskType));
final long beforePerform = this.logger.isDebugEnabled() ? System.currentTimeMillis() : 0;
try {
task.perform(); // This can block for a while
@@ -231,29 +223,25 @@ public abstract class ExecuteProduceConsume implements Runnable {
// We're in shutdown situation so exit
Thread.currentThread().interrupt();
} catch (Exception e) {
this.logger.warn(() -> String.format("[%d] exception while performing task", Thread.currentThread().getId()), e);
this.logger.warn(() -> String.format("[%d] exception while consuming task", Thread.currentThread().getId()), e);
}
if (this.isLoggerTraceEnabled) {
this.logger.trace(() -> String.format("[%d] finished task", Thread.currentThread().getId()));
if (this.logger.isDebugEnabled()) {
final long productionPeriod = System.currentTimeMillis() - beforePerform;
this.logger.debug(() -> String.format("[%d] consumed [%s] task in %dms", Thread.currentThread().getId(), taskType, productionPeriod));
}
synchronized (this) {
++this.tasksConsumed;
--this.consumerCount;
if (this.isLoggerTraceEnabled) {
this.logger.trace(() -> String.format("[%d] consumerCount now: %d",
Thread.currentThread().getId(), this.consumerCount));
}
// Quicker, non-blocking produce next round
canBlock = false;
this.logger.trace(() -> String.format("[%d] consumerCount now: %d",
Thread.currentThread().getId(), this.consumerCount));
}
}
} finally {
if (this.isLoggerTraceEnabled)
Thread.currentThread().setName(this.className);
Thread.currentThread().setName(this.className);
}
}

View File

@@ -13,9 +13,25 @@ import org.junit.Test;
import org.qortal.utils.ExecuteProduceConsume;
import org.qortal.utils.ExecuteProduceConsume.StatsSnapshot;
import static org.junit.Assert.fail;
public class EPCTests {
class RandomEPC extends ExecuteProduceConsume {
static class SleepTask implements ExecuteProduceConsume.Task {
private static final Random RANDOM = new Random();
@Override
public String getName() {
return "SleepTask";
}
@Override
public void perform() throws InterruptedException {
Thread.sleep(RANDOM.nextInt(500) + 100);
}
}
static class RandomEPC extends ExecuteProduceConsume {
private final int TASK_PERCENT;
private final int PAUSE_PERCENT;
@@ -37,9 +53,7 @@ public class EPCTests {
// Sometimes produce a task
if (percent < TASK_PERCENT) {
return () -> {
Thread.sleep(random.nextInt(500) + 100);
};
return new SleepTask();
} else {
// If we don't produce a task, then maybe simulate a pause until work arrives
if (canIdle && percent < PAUSE_PERCENT)
@@ -50,45 +64,6 @@ public class EPCTests {
}
}
private void testEPC(ExecuteProduceConsume testEPC) throws InterruptedException {
final int runTime = 60; // seconds
System.out.println(String.format("Testing EPC for %s seconds:", runTime));
final long start = System.currentTimeMillis();
testEPC.start();
// Status reports every second (bar waiting for synchronization)
ScheduledExecutorService statusExecutor = Executors.newSingleThreadScheduledExecutor();
statusExecutor.scheduleAtFixedRate(() -> {
final StatsSnapshot snapshot = testEPC.getStatsSnapshot();
final long seconds = (System.currentTimeMillis() - start) / 1000L;
System.out.print(String.format("After %d second%s, ", seconds, (seconds != 1 ? "s" : "")));
printSnapshot(snapshot);
}, 1L, 1L, TimeUnit.SECONDS);
// Let it run for a minute
Thread.sleep(runTime * 1000L);
statusExecutor.shutdownNow();
final long before = System.currentTimeMillis();
testEPC.shutdown(30 * 1000);
final long after = System.currentTimeMillis();
System.out.println(String.format("Shutdown took %d milliseconds", after - before));
final StatsSnapshot snapshot = testEPC.getStatsSnapshot();
System.out.print("After shutdown, ");
printSnapshot(snapshot);
}
private void printSnapshot(final StatsSnapshot snapshot) {
System.out.println(String.format("threads: %d active (%d max, %d exhaustion%s), tasks: %d produced / %d consumed",
snapshot.activeThreadCount, snapshot.greatestActiveThreadCount,
snapshot.spawnFailures, (snapshot.spawnFailures != 1 ? "s": ""),
snapshot.tasksProduced, snapshot.tasksConsumed));
}
@Test
public void testRandomEPC() throws InterruptedException {
final int TASK_PERCENT = 25; // Produce a task this % of the time
@@ -131,18 +106,39 @@ public class EPCTests {
final int MAX_PEERS = 20;
final List<Long> lastPings = new ArrayList<>(Collections.nCopies(MAX_PEERS, System.currentTimeMillis()));
final List<Long> lastPingProduced = new ArrayList<>(Collections.nCopies(MAX_PEERS, System.currentTimeMillis()));
class PingTask implements ExecuteProduceConsume.Task {
private final int peerIndex;
private final long lastPing;
private final long productionTimestamp;
private final String name;
public PingTask(int peerIndex) {
public PingTask(int peerIndex, long lastPing, long productionTimestamp) {
this.peerIndex = peerIndex;
this.lastPing = lastPing;
this.productionTimestamp = productionTimestamp;
this.name = "PingTask::[" + this.peerIndex + "]";
}
@Override
public String getName() {
return name;
}
@Override
public void perform() throws InterruptedException {
System.out.println("Pinging peer " + peerIndex);
long now = System.currentTimeMillis();
System.out.println(String.format("Pinging peer %d after post-production delay of %dms and ping interval of %dms",
peerIndex,
now - productionTimestamp,
now - lastPing
));
long threshold = now - PING_INTERVAL - PRODUCER_SLEEP_TIME;
if (lastPing < threshold)
fail("excessive peer ping interval for peer " + peerIndex);
// At least half the worst case ping round-trip
Random random = new Random();
@@ -155,32 +151,73 @@ public class EPCTests {
class PingEPC extends ExecuteProduceConsume {
@Override
protected Task produceTask(boolean canIdle) throws InterruptedException {
// If we can idle, then we do, to simulate worst case
if (canIdle)
Thread.sleep(PRODUCER_SLEEP_TIME);
// Is there a peer that needs a ping?
final long now = System.currentTimeMillis();
synchronized (lastPings) {
for (int peerIndex = 0; peerIndex < lastPings.size(); ++peerIndex) {
long lastPing = lastPings.get(peerIndex);
if (lastPing < now - PING_INTERVAL - PING_ROUND_TRIP_TIME - PRODUCER_SLEEP_TIME)
throw new RuntimeException("excessive peer ping interval for peer " + peerIndex);
synchronized (lastPingProduced) {
for (int peerIndex = 0; peerIndex < lastPingProduced.size(); ++peerIndex) {
long lastPing = lastPingProduced.get(peerIndex);
if (lastPing < now - PING_INTERVAL) {
lastPings.set(peerIndex, System.currentTimeMillis());
return new PingTask(peerIndex);
lastPingProduced.set(peerIndex, System.currentTimeMillis());
return new PingTask(peerIndex, lastPing, now);
}
}
}
// If we can idle, then we do, to simulate worst case
if (canIdle)
Thread.sleep(PRODUCER_SLEEP_TIME);
// No work to do
return null;
}
}
System.out.println(String.format("Pings should start after %s seconds", PING_INTERVAL));
testEPC(new PingEPC());
}
private void testEPC(ExecuteProduceConsume testEPC) throws InterruptedException {
final int runTime = 60; // seconds
System.out.println(String.format("Testing EPC for %s seconds:", runTime));
final long start = System.currentTimeMillis();
// Status reports every second (bar waiting for synchronization)
ScheduledExecutorService statusExecutor = Executors.newSingleThreadScheduledExecutor();
statusExecutor.scheduleAtFixedRate(
() -> {
final StatsSnapshot snapshot = testEPC.getStatsSnapshot();
final long seconds = (System.currentTimeMillis() - start) / 1000L;
System.out.println(String.format("After %d second%s, %s", seconds, seconds != 1 ? "s" : "", formatSnapshot(snapshot)));
},
0L, 1L, TimeUnit.SECONDS
);
testEPC.start();
// Let it run for a minute
Thread.sleep(runTime * 1000L);
statusExecutor.shutdownNow();
final long before = System.currentTimeMillis();
testEPC.shutdown(30 * 1000);
final long after = System.currentTimeMillis();
System.out.println(String.format("Shutdown took %d milliseconds", after - before));
final StatsSnapshot snapshot = testEPC.getStatsSnapshot();
System.out.println("After shutdown, " + formatSnapshot(snapshot));
}
private String formatSnapshot(StatsSnapshot snapshot) {
return String.format("threads: %d active (%d max, %d exhaustion%s), tasks: %d produced / %d consumed",
snapshot.activeThreadCount, snapshot.greatestActiveThreadCount,
snapshot.spawnFailures, (snapshot.spawnFailures != 1 ? "s": ""),
snapshot.tasksProduced, snapshot.tasksConsumed
);
}
}

View File

@@ -0,0 +1,115 @@
package org.qortal.test.crosschain;
import static org.junit.Assert.*;
import java.util.Arrays;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.store.BlockStoreException;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.Digibyte;
import org.qortal.crosschain.BitcoinyHTLC;
import org.qortal.repository.DataException;
import org.qortal.test.common.Common;
public class DigibyteTests extends Common {
private Digibyte digibyte;
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings(); // TestNet3
digibyte = Digibyte.getInstance();
}
@After
public void afterTest() {
Digibyte.resetForTesting();
digibyte = null;
}
@Test
public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException {
long before = System.currentTimeMillis();
System.out.println(String.format("Digibyte median blocktime: %d", digibyte.getMedianBlockTime()));
long afterFirst = System.currentTimeMillis();
System.out.println(String.format("Digibyte median blocktime: %d", digibyte.getMedianBlockTime()));
long afterSecond = System.currentTimeMillis();
long firstPeriod = afterFirst - before;
long secondPeriod = afterSecond - afterFirst;
System.out.println(String.format("1st call: %d ms, 2nd call: %d ms", firstPeriod, secondPeriod));
assertTrue("2nd call should be quicker than 1st", secondPeriod < firstPeriod);
assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L);
}
@Test
@Ignore(value = "Doesn't work, to be fixed later")
public void testFindHtlcSecret() throws ForeignBlockchainException {
// This actually exists on TEST3 but can take a while to fetch
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
byte[] secret = BitcoinyHTLC.findHtlcSecret(digibyte, p2shAddress);
assertNotNull("secret not found", secret);
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
}
@Test
@Ignore(value = "No testnet nodes available, so we can't regularly test buildSpend yet")
public void testBuildSpend() {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
String recipient = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
long amount = 1000L;
Transaction transaction = digibyte.buildSpend(xprv58, recipient, amount);
assertNotNull("insufficient funds", transaction);
// Check spent key caching doesn't affect outcome
transaction = digibyte.buildSpend(xprv58, recipient, amount);
assertNotNull("insufficient funds", transaction);
}
@Test
public void testGetWalletBalance() {
String xprv58 = "xpub661MyMwAqRbcEnabTLX5uebYcsE3uG5y7ve9jn1VK8iY1MaU3YLoLJEe8sTu2YVav5Zka5qf2dmMssfxmXJTqZnazZL2kL7M2tNKwEoC34R";
Long balance = digibyte.getWalletBalance(xprv58);
assertNotNull(balance);
System.out.println(digibyte.format(balance));
// Check spent key caching doesn't affect outcome
Long repeatBalance = digibyte.getWalletBalance(xprv58);
assertNotNull(repeatBalance);
System.out.println(digibyte.format(repeatBalance));
assertEquals(balance, repeatBalance);
}
@Test
public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
String xprv58 = "xpub661MyMwAqRbcEnabTLX5uebYcsE3uG5y7ve9jn1VK8iY1MaU3YLoLJEe8sTu2YVav5Zka5qf2dmMssfxmXJTqZnazZL2kL7M2tNKwEoC34R";
String address = digibyte.getUnusedReceiveAddress(xprv58);
assertNotNull(address);
System.out.println(address);
}
}

View File

@@ -0,0 +1,115 @@
package org.qortal.test.crosschain;
import static org.junit.Assert.*;
import java.util.Arrays;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.store.BlockStoreException;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.Ravencoin;
import org.qortal.crosschain.BitcoinyHTLC;
import org.qortal.repository.DataException;
import org.qortal.test.common.Common;
public class RavencoinTests extends Common {
private Ravencoin ravencoin;
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings(); // TestNet3
ravencoin = Ravencoin.getInstance();
}
@After
public void afterTest() {
Ravencoin.resetForTesting();
ravencoin = null;
}
@Test
public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException {
long before = System.currentTimeMillis();
System.out.println(String.format("Ravencoin median blocktime: %d", ravencoin.getMedianBlockTime()));
long afterFirst = System.currentTimeMillis();
System.out.println(String.format("Ravencoin median blocktime: %d", ravencoin.getMedianBlockTime()));
long afterSecond = System.currentTimeMillis();
long firstPeriod = afterFirst - before;
long secondPeriod = afterSecond - afterFirst;
System.out.println(String.format("1st call: %d ms, 2nd call: %d ms", firstPeriod, secondPeriod));
assertTrue("2nd call should be quicker than 1st", secondPeriod < firstPeriod);
assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L);
}
@Test
@Ignore(value = "Doesn't work, to be fixed later")
public void testFindHtlcSecret() throws ForeignBlockchainException {
// This actually exists on TEST3 but can take a while to fetch
String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
byte[] secret = BitcoinyHTLC.findHtlcSecret(ravencoin, p2shAddress);
assertNotNull("secret not found", secret);
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
}
@Test
@Ignore(value = "No testnet nodes available, so we can't regularly test buildSpend yet")
public void testBuildSpend() {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
String recipient = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
long amount = 1000L;
Transaction transaction = ravencoin.buildSpend(xprv58, recipient, amount);
assertNotNull("insufficient funds", transaction);
// Check spent key caching doesn't affect outcome
transaction = ravencoin.buildSpend(xprv58, recipient, amount);
assertNotNull("insufficient funds", transaction);
}
@Test
public void testGetWalletBalance() {
String xprv58 = "xpub661MyMwAqRbcEt3Ge1wNmkagyb1J7yTQu4Kquvy77Ycg2iPoh7Urg8s9Jdwp7YmrqGkDKJpUVjsZXSSsQgmAVUC17ZVQQeoWMzm7vDTt1y7";
Long balance = ravencoin.getWalletBalance(xprv58);
assertNotNull(balance);
System.out.println(ravencoin.format(balance));
// Check spent key caching doesn't affect outcome
Long repeatBalance = ravencoin.getWalletBalance(xprv58);
assertNotNull(repeatBalance);
System.out.println(ravencoin.format(repeatBalance));
assertEquals(balance, repeatBalance);
}
@Test
public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
String xprv58 = "xpub661MyMwAqRbcEt3Ge1wNmkagyb1J7yTQu4Kquvy77Ycg2iPoh7Urg8s9Jdwp7YmrqGkDKJpUVjsZXSSsQgmAVUC17ZVQQeoWMzm7vDTt1y7";
String address = ravencoin.getUnusedReceiveAddress(xprv58);
assertNotNull(address);
System.out.println(address);
}
}

View File

@@ -0,0 +1,769 @@
package org.qortal.test.crosschain.digibytev3;
import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes;
import org.junit.Before;
import org.junit.Test;
import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.block.Block;
import org.qortal.crosschain.AcctMode;
import org.qortal.crosschain.DigibyteACCTv3;
import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData;
import org.qortal.data.crosschain.CrossChainTradeData;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.BlockUtils;
import org.qortal.test.common.Common;
import org.qortal.test.common.TransactionUtils;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.utils.Amounts;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.function.Function;
import static org.junit.Assert.*;
public class DigibyteACCTv3Tests extends Common {
public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes();
public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
public static final byte[] digibytePublicKeyHash = HashCode.fromString("bb00bb11bb22bb33bb44bb55bb66bb77bb88bb99").asBytes();
public static final int tradeTimeout = 20; // blocks
public static final long redeemAmount = 80_40200000L;
public static final long fundingAmount = 123_45600000L;
public static final long digibyteAmount = 864200L; // 0.00864200 DGB
private static final Random RANDOM = new Random();
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
}
@Test
public void testCompile() {
PrivateKeyAccount tradeAccount = createTradeAccount(null);
byte[] creationBytes = DigibyteACCTv3.buildQortalAT(tradeAccount.getAddress(), digibytePublicKeyHash, redeemAmount, digibyteAmount, tradeTimeout);
assertNotNull(creationBytes);
System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
}
@Test
public void testDeploy() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
long expectedBalance = deployersInitialBalance - fundingAmount - deployAtTransaction.getTransactionData().getFee();
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = fundingAmount;
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
assertEquals("AT's post-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = partnersInitialBalance;
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-deployment balance incorrect", expectedBalance, actualBalance);
// Test orphaning
BlockUtils.orphanLastBlock(repository);
expectedBalance = deployersInitialBalance;
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = 0;
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
assertEquals("AT's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = partnersInitialBalance;
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
}
}
@SuppressWarnings("unused")
@Test
public void testOfferCancel() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
// Send creator's address to AT, instead of typical partner's address
byte[] messageData = DigibyteACCTv3.getInstance().buildCancelMessage(deployer.getAddress());
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
long messageFee = messageTransaction.getTransactionData().getFee();
// AT should process 'cancel' message in next block
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in CANCELLED mode
CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.CANCELLED, tradeData.mode);
// Check balances
long expectedMinimumBalance = deployersPostDeploymentBalance;
long expectedMaximumBalance = deployersInitialBalance - deployAtFee - messageFee;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
// Test orphaning
BlockUtils.orphanLastBlock(repository);
// Check balances
long expectedBalance = deployersPostDeploymentBalance - messageFee;
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
}
@SuppressWarnings("unused")
@Test
public void testOfferCancelInvalidLength() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
// Instead of sending creator's address to AT, send too-short/invalid message
byte[] messageData = new byte[7];
RANDOM.nextBytes(messageData);
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
long messageFee = messageTransaction.getTransactionData().getFee();
// AT should process 'cancel' message in next block
// As message is too short, it will be padded to 32bytes but cancel code doesn't care about message content, so should be ok
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in CANCELLED mode
CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.CANCELLED, tradeData.mode);
}
}
@SuppressWarnings("unused")
@Test
public void testTradingInfoProcessing() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = DigibyteACCTv3.buildTradeMessage(partner.getAddress(), digibytePublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
describeAt(repository, atAddress);
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
// AT should be in TRADE mode
assertEquals(AcctMode.TRADING, tradeData.mode);
// Check hashOfSecretA was extracted correctly
assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
// Check trade partner Qortal address was extracted correctly
assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
// Check trade partner's digibyte PKH was extracted correctly
assertTrue(Arrays.equals(digibytePublicKeyHash, tradeData.partnerForeignPKH));
// Test orphaning
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
// Check balances
long expectedBalance = deployersPostDeploymentBalance;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
}
// TEST SENDING TRADING INFO BUT NOT FROM AT CREATOR (SHOULD BE IGNORED)
@SuppressWarnings("unused")
@Test
public void testIncorrectTradeSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT BUT NOT FROM AT CREATOR
byte[] messageData = DigibyteACCTv3.buildTradeMessage(partner.getAddress(), digibytePublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
BlockUtils.mintBlock(repository);
long expectedBalance = partnersInitialBalance;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance);
describeAt(repository, atAddress);
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
// AT should still be in OFFER mode
assertEquals(AcctMode.OFFERING, tradeData.mode);
}
}
@SuppressWarnings("unused")
@Test
public void testAutomaticTradeRefund() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = DigibyteACCTv3.buildTradeMessage(partner.getAddress(), digibytePublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
// Check refund
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in REFUNDED mode
CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.REFUNDED, tradeData.mode);
// Test orphaning
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
// Check balances
long expectedBalance = deployersPostDeploymentBalance;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
}
@SuppressWarnings("unused")
@Test
public void testCorrectSecretCorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = DigibyteACCTv3.buildTradeMessage(partner.getAddress(), digibytePublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secret to AT, from correct account
messageData = DigibyteACCTv3.buildRedeemMessage(secretA, partner.getAddress());
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in REDEEMED mode
CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.REDEEMED, tradeData.mode);
// Check balances
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance);
// Orphan redeem
BlockUtils.orphanLastBlock(repository);
// Check balances
expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance);
// Check AT state
ATStateData postOrphanAtStateData = repository.getATRepository().getLatestATState(atAddress);
assertTrue("AT states mismatch", Arrays.equals(preRedeemAtStateData.getStateData(), postOrphanAtStateData.getStateData()));
}
}
@SuppressWarnings("unused")
@Test
public void testCorrectSecretIncorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = DigibyteACCTv3.buildTradeMessage(partner.getAddress(), digibytePublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secret to AT, but from wrong account
messageData = DigibyteACCTv3.buildRedeemMessage(secretA, partner.getAddress());
messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should still be in TRADE mode
CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
// Check balances
long expectedBalance = partnersInitialBalance;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
// Check eventual refund
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
}
}
@SuppressWarnings("unused")
@Test
public void testIncorrectSecretCorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = DigibyteACCTv3.buildTradeMessage(partner.getAddress(), digibytePublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send incorrect secret to AT, from correct account
byte[] wrongSecret = new byte[32];
RANDOM.nextBytes(wrongSecret);
messageData = DigibyteACCTv3.buildRedeemMessage(wrongSecret, partner.getAddress());
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should still be in TRADE mode
CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
// Check eventual refund
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
}
}
@SuppressWarnings("unused")
@Test
public void testCorrectSecretCorrectSenderInvalidMessageLength() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = DigibyteACCTv3.buildTradeMessage(partner.getAddress(), digibytePublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secret to AT, from correct account, but missing receive address, hence incorrect length
messageData = Bytes.concat(secretA);
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should be in TRADING mode
CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
}
}
@SuppressWarnings("unused")
@Test
public void testDescribeDeployed() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
List<ATData> executableAts = repository.getATRepository().getAllExecutableATs();
for (ATData atData : executableAts) {
String atAddress = atData.getATAddress();
byte[] codeBytes = atData.getCodeBytes();
byte[] codeHash = Crypto.digest(codeBytes);
System.out.println(String.format("%s: code length: %d byte%s, code hash: %s",
atAddress,
codeBytes.length,
(codeBytes.length != 1 ? "s": ""),
HashCode.fromBytes(codeHash)));
// Not one of ours?
if (!Arrays.equals(codeHash, DigibyteACCTv3.CODE_BYTES_HASH))
continue;
describeAt(repository, atAddress);
}
}
}
private int calcTestLockTimeA(long messageTimestamp) {
return (int) (messageTimestamp / 1000L + tradeTimeout * 60);
}
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException {
byte[] creationBytes = DigibyteACCTv3.buildQortalAT(tradeAddress, digibytePublicKeyHash, redeemAmount, digibyteAmount, tradeTimeout);
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = deployer.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress()));
System.exit(2);
}
Long fee = null;
String name = "QORT-DGB cross-chain trade";
String description = String.format("Qortal-Digibyte cross-chain trade");
String atType = "ACCT";
String tags = "QORT-DGB ACCT";
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null);
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT);
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
fee = deployAtTransaction.calcRecommendedFee();
deployAtTransactionData.setFee(fee);
TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer);
return deployAtTransaction;
}
private MessageTransaction sendMessage(Repository repository, PrivateKeyAccount sender, byte[] data, String recipient) throws DataException {
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = sender.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", sender.getAddress()));
System.exit(2);
}
Long fee = null;
int version = 4;
int nonce = 0;
long amount = 0;
Long assetId = null; // because amount is zero
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, data, false, false);
MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);
fee = messageTransaction.calcRecommendedFee();
messageTransactionData.setFee(fee);
TransactionUtils.signAndMint(repository, messageTransactionData, sender);
return messageTransaction;
}
private void checkTradeRefund(Repository repository, Account deployer, long deployersInitialBalance, long deployAtFee) throws DataException {
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
int refundTimeout = tradeTimeout / 2 + 1; // close enough
// AT should automatically refund deployer after 'refundTimeout' blocks
for (int blockCount = 0; blockCount <= refundTimeout; ++blockCount)
BlockUtils.mintBlock(repository);
// We don't bother to exactly calculate QORT spent running AT for several blocks, but we do know the expected range
long expectedMinimumBalance = deployersPostDeploymentBalance;
long expectedMaximumBalance = deployersInitialBalance - deployAtFee;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
}
private void describeAt(Repository repository, String atAddress) throws DataException {
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
Function<Long, String> epochMilliFormatter = (timestamp) -> LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC).format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM));
int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight();
System.out.print(String.format("%s:\n"
+ "\tmode: %s\n"
+ "\tcreator: %s,\n"
+ "\tcreation timestamp: %s,\n"
+ "\tcurrent balance: %s QORT,\n"
+ "\tis finished: %b,\n"
+ "\tredeem payout: %s QORT,\n"
+ "\texpected digibyte: %s DGB,\n"
+ "\tcurrent block height: %d,\n",
tradeData.qortalAtAddress,
tradeData.mode,
tradeData.qortalCreator,
epochMilliFormatter.apply(tradeData.creationTimestamp),
Amounts.prettyAmount(tradeData.qortBalance),
atData.getIsFinished(),
Amounts.prettyAmount(tradeData.qortAmount),
Amounts.prettyAmount(tradeData.expectedForeignAmount),
currentBlockHeight));
if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
System.out.println(String.format("\trefund timeout: %d minutes,\n"
+ "\trefund height: block %d,\n"
+ "\tHASH160 of secret-A: %s,\n"
+ "\tDigibyte P2SH-A nLockTime: %d (%s),\n"
+ "\ttrade partner: %s\n"
+ "\tpartner's receiving address: %s",
tradeData.refundTimeout,
tradeData.tradeRefundHeight,
HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40),
tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L),
tradeData.qortalPartnerAddress,
tradeData.qortalPartnerReceivingAddress));
}
}
private PrivateKeyAccount createTradeAccount(Repository repository) {
// We actually use a known test account with funds to avoid PoW compute
return Common.getTestAccount(repository, "alice");
}
}

View File

@@ -0,0 +1,769 @@
package org.qortal.test.crosschain.ravencoinv3;
import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes;
import org.junit.Before;
import org.junit.Test;
import org.qortal.account.Account;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.asset.Asset;
import org.qortal.block.Block;
import org.qortal.crosschain.AcctMode;
import org.qortal.crosschain.RavencoinACCTv3;
import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData;
import org.qortal.data.at.ATStateData;
import org.qortal.data.crosschain.CrossChainTradeData;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.DeployAtTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.test.common.BlockUtils;
import org.qortal.test.common.Common;
import org.qortal.test.common.TransactionUtils;
import org.qortal.transaction.DeployAtTransaction;
import org.qortal.transaction.MessageTransaction;
import org.qortal.utils.Amounts;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.function.Function;
import static org.junit.Assert.*;
public class RavencoinACCTv3Tests extends Common {
public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes();
public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
public static final byte[] ravencoinPublicKeyHash = HashCode.fromString("bb00bb11bb22bb33bb44bb55bb66bb77bb88bb99").asBytes();
public static final int tradeTimeout = 20; // blocks
public static final long redeemAmount = 80_40200000L;
public static final long fundingAmount = 123_45600000L;
public static final long ravencoinAmount = 864200L; // 0.00864200 RVN
private static final Random RANDOM = new Random();
@Before
public void beforeTest() throws DataException {
Common.useDefaultSettings();
}
@Test
public void testCompile() {
PrivateKeyAccount tradeAccount = createTradeAccount(null);
byte[] creationBytes = RavencoinACCTv3.buildQortalAT(tradeAccount.getAddress(), ravencoinPublicKeyHash, redeemAmount, ravencoinAmount, tradeTimeout);
assertNotNull(creationBytes);
System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
}
@Test
public void testDeploy() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
long expectedBalance = deployersInitialBalance - fundingAmount - deployAtTransaction.getTransactionData().getFee();
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = fundingAmount;
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
assertEquals("AT's post-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = partnersInitialBalance;
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-deployment balance incorrect", expectedBalance, actualBalance);
// Test orphaning
BlockUtils.orphanLastBlock(repository);
expectedBalance = deployersInitialBalance;
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = 0;
actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
assertEquals("AT's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
expectedBalance = partnersInitialBalance;
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
}
}
@SuppressWarnings("unused")
@Test
public void testOfferCancel() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
// Send creator's address to AT, instead of typical partner's address
byte[] messageData = RavencoinACCTv3.getInstance().buildCancelMessage(deployer.getAddress());
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
long messageFee = messageTransaction.getTransactionData().getFee();
// AT should process 'cancel' message in next block
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in CANCELLED mode
CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.CANCELLED, tradeData.mode);
// Check balances
long expectedMinimumBalance = deployersPostDeploymentBalance;
long expectedMaximumBalance = deployersInitialBalance - deployAtFee - messageFee;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
// Test orphaning
BlockUtils.orphanLastBlock(repository);
// Check balances
long expectedBalance = deployersPostDeploymentBalance - messageFee;
actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
}
@SuppressWarnings("unused")
@Test
public void testOfferCancelInvalidLength() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
// Instead of sending creator's address to AT, send too-short/invalid message
byte[] messageData = new byte[7];
RANDOM.nextBytes(messageData);
MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
long messageFee = messageTransaction.getTransactionData().getFee();
// AT should process 'cancel' message in next block
// As message is too short, it will be padded to 32bytes but cancel code doesn't care about message content, so should be ok
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in CANCELLED mode
CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.CANCELLED, tradeData.mode);
}
}
@SuppressWarnings("unused")
@Test
public void testTradingInfoProcessing() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = RavencoinACCTv3.buildTradeMessage(partner.getAddress(), ravencoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
describeAt(repository, atAddress);
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
// AT should be in TRADE mode
assertEquals(AcctMode.TRADING, tradeData.mode);
// Check hashOfSecretA was extracted correctly
assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
// Check trade partner Qortal address was extracted correctly
assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
// Check trade partner's ravencoin PKH was extracted correctly
assertTrue(Arrays.equals(ravencoinPublicKeyHash, tradeData.partnerForeignPKH));
// Test orphaning
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
// Check balances
long expectedBalance = deployersPostDeploymentBalance;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
}
// TEST SENDING TRADING INFO BUT NOT FROM AT CREATOR (SHOULD BE IGNORED)
@SuppressWarnings("unused")
@Test
public void testIncorrectTradeSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT BUT NOT FROM AT CREATOR
byte[] messageData = RavencoinACCTv3.buildTradeMessage(partner.getAddress(), ravencoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
BlockUtils.mintBlock(repository);
long expectedBalance = partnersInitialBalance;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance);
describeAt(repository, atAddress);
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
// AT should still be in OFFER mode
assertEquals(AcctMode.OFFERING, tradeData.mode);
}
}
@SuppressWarnings("unused")
@Test
public void testAutomaticTradeRefund() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = RavencoinACCTv3.buildTradeMessage(partner.getAddress(), ravencoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
Block postDeploymentBlock = BlockUtils.mintBlock(repository);
int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
// Check refund
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in REFUNDED mode
CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.REFUNDED, tradeData.mode);
// Test orphaning
BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
// Check balances
long expectedBalance = deployersPostDeploymentBalance;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
}
@SuppressWarnings("unused")
@Test
public void testCorrectSecretCorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = RavencoinACCTv3.buildTradeMessage(partner.getAddress(), ravencoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secret to AT, from correct account
messageData = RavencoinACCTv3.buildRedeemMessage(secretA, partner.getAddress());
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertTrue(atData.getIsFinished());
// AT should be in REDEEMED mode
CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.REDEEMED, tradeData.mode);
// Check balances
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance);
// Orphan redeem
BlockUtils.orphanLastBlock(repository);
// Check balances
expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance);
// Check AT state
ATStateData postOrphanAtStateData = repository.getATRepository().getLatestATState(atAddress);
assertTrue("AT states mismatch", Arrays.equals(preRedeemAtStateData.getStateData(), postOrphanAtStateData.getStateData()));
}
}
@SuppressWarnings("unused")
@Test
public void testCorrectSecretIncorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = RavencoinACCTv3.buildTradeMessage(partner.getAddress(), ravencoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secret to AT, but from wrong account
messageData = RavencoinACCTv3.buildRedeemMessage(secretA, partner.getAddress());
messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should still be in TRADE mode
CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
// Check balances
long expectedBalance = partnersInitialBalance;
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
// Check eventual refund
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
}
}
@SuppressWarnings("unused")
@Test
public void testIncorrectSecretCorrectSender() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
long deployAtFee = deployAtTransaction.getTransactionData().getFee();
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = RavencoinACCTv3.buildTradeMessage(partner.getAddress(), ravencoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send incorrect secret to AT, from correct account
byte[] wrongSecret = new byte[32];
RANDOM.nextBytes(wrongSecret);
messageData = RavencoinACCTv3.buildRedeemMessage(wrongSecret, partner.getAddress());
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should still be in TRADE mode
CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
long actualBalance = partner.getConfirmedBalance(Asset.QORT);
assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
// Check eventual refund
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
}
}
@SuppressWarnings("unused")
@Test
public void testCorrectSecretCorrectSenderInvalidMessageLength() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
Account at = deployAtTransaction.getATAccount();
String atAddress = at.getAddress();
long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
int refundTimeout = RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
// Send trade info to AT
byte[] messageData = RavencoinACCTv3.buildTradeMessage(partner.getAddress(), ravencoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
// Give AT time to process message
BlockUtils.mintBlock(repository);
// Send correct secret to AT, from correct account, but missing receive address, hence incorrect length
messageData = Bytes.concat(secretA);
messageTransaction = sendMessage(repository, partner, messageData, atAddress);
// AT should NOT send funds in the next block
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
describeAt(repository, atAddress);
// Check AT is NOT finished
ATData atData = repository.getATRepository().fromATAddress(atAddress);
assertFalse(atData.getIsFinished());
// AT should be in TRADING mode
CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
assertEquals(AcctMode.TRADING, tradeData.mode);
}
}
@SuppressWarnings("unused")
@Test
public void testDescribeDeployed() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount tradeAccount = createTradeAccount(repository);
PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
List<ATData> executableAts = repository.getATRepository().getAllExecutableATs();
for (ATData atData : executableAts) {
String atAddress = atData.getATAddress();
byte[] codeBytes = atData.getCodeBytes();
byte[] codeHash = Crypto.digest(codeBytes);
System.out.println(String.format("%s: code length: %d byte%s, code hash: %s",
atAddress,
codeBytes.length,
(codeBytes.length != 1 ? "s": ""),
HashCode.fromBytes(codeHash)));
// Not one of ours?
if (!Arrays.equals(codeHash, RavencoinACCTv3.CODE_BYTES_HASH))
continue;
describeAt(repository, atAddress);
}
}
}
private int calcTestLockTimeA(long messageTimestamp) {
return (int) (messageTimestamp / 1000L + tradeTimeout * 60);
}
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException {
byte[] creationBytes = RavencoinACCTv3.buildQortalAT(tradeAddress, ravencoinPublicKeyHash, redeemAmount, ravencoinAmount, tradeTimeout);
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = deployer.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress()));
System.exit(2);
}
Long fee = null;
String name = "QORT-RVN cross-chain trade";
String description = String.format("Qortal-Ravencoin cross-chain trade");
String atType = "ACCT";
String tags = "QORT-RVN ACCT";
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null);
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT);
DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
fee = deployAtTransaction.calcRecommendedFee();
deployAtTransactionData.setFee(fee);
TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer);
return deployAtTransaction;
}
private MessageTransaction sendMessage(Repository repository, PrivateKeyAccount sender, byte[] data, String recipient) throws DataException {
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = sender.getLastReference();
if (lastReference == null) {
System.err.println(String.format("Qortal account %s has no last reference", sender.getAddress()));
System.exit(2);
}
Long fee = null;
int version = 4;
int nonce = 0;
long amount = 0;
Long assetId = null; // because amount is zero
BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, data, false, false);
MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);
fee = messageTransaction.calcRecommendedFee();
messageTransactionData.setFee(fee);
TransactionUtils.signAndMint(repository, messageTransactionData, sender);
return messageTransaction;
}
private void checkTradeRefund(Repository repository, Account deployer, long deployersInitialBalance, long deployAtFee) throws DataException {
long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
int refundTimeout = tradeTimeout / 2 + 1; // close enough
// AT should automatically refund deployer after 'refundTimeout' blocks
for (int blockCount = 0; blockCount <= refundTimeout; ++blockCount)
BlockUtils.mintBlock(repository);
// We don't bother to exactly calculate QORT spent running AT for several blocks, but we do know the expected range
long expectedMinimumBalance = deployersPostDeploymentBalance;
long expectedMaximumBalance = deployersInitialBalance - deployAtFee;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
}
private void describeAt(Repository repository, String atAddress) throws DataException {
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
Function<Long, String> epochMilliFormatter = (timestamp) -> LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC).format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM));
int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight();
System.out.print(String.format("%s:\n"
+ "\tmode: %s\n"
+ "\tcreator: %s,\n"
+ "\tcreation timestamp: %s,\n"
+ "\tcurrent balance: %s QORT,\n"
+ "\tis finished: %b,\n"
+ "\tredeem payout: %s QORT,\n"
+ "\texpected ravencoin: %s RVN,\n"
+ "\tcurrent block height: %d,\n",
tradeData.qortalAtAddress,
tradeData.mode,
tradeData.qortalCreator,
epochMilliFormatter.apply(tradeData.creationTimestamp),
Amounts.prettyAmount(tradeData.qortBalance),
atData.getIsFinished(),
Amounts.prettyAmount(tradeData.qortAmount),
Amounts.prettyAmount(tradeData.expectedForeignAmount),
currentBlockHeight));
if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
System.out.println(String.format("\trefund timeout: %d minutes,\n"
+ "\trefund height: block %d,\n"
+ "\tHASH160 of secret-A: %s,\n"
+ "\tRavencoin P2SH-A nLockTime: %d (%s),\n"
+ "\ttrade partner: %s\n"
+ "\tpartner's receiving address: %s",
tradeData.refundTimeout,
tradeData.tradeRefundHeight,
HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40),
tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L),
tradeData.qortalPartnerAddress,
tradeData.qortalPartnerReceivingAddress));
}
}
private PrivateKeyAccount createTradeAccount(Repository repository) {
// We actually use a known test account with funds to avoid PoW compute
return Common.getTestAccount(repository, "alice");
}
}

View File

@@ -29,7 +29,7 @@ public class OnlineAccountsTests {
@Test
public void testGetOnlineAccountsV2() throws Message.MessageException {
public void testGetOnlineAccountsV2() throws MessageException {
List<OnlineAccountData> onlineAccountsOut = generateOnlineAccounts(false);
Message messageOut = new GetOnlineAccountsV2Message(onlineAccountsOut);
@@ -58,7 +58,7 @@ public class OnlineAccountsTests {
}
@Test
public void testOnlineAccountsV2() throws Message.MessageException {
public void testOnlineAccountsV2() throws MessageException {
List<OnlineAccountData> onlineAccountsOut = generateOnlineAccounts(true);
Message messageOut = new OnlineAccountsV2Message(onlineAccountsOut);