Compare commits

...

23 Commits

Author SHA1 Message Date
CalDescent
cd7adc997b Prevent duplicate entries in a list. 2021-08-07 11:32:49 +01:00
CalDescent
9fdc901b7a Added POST /lists/blacklist/addresses and DELETE /lists/blacklist/addresses API endpoints.
These are the same as the /lists/blacklist/address/{address} endpoints but allow a JSON array of addresses to be specified in the request body. They currently return true if
2021-08-07 11:31:45 +01:00
CalDescent
b29ae67501 Apply the address blacklist to chat transactions.
This is based on code originally written by @DrewMPeacock
2021-08-07 10:31:56 +01:00
CalDescent
24f1fb566d Initial implementation of resource lists
The ResourceList class creates or updates a list for the purpose of tracking resources on the Qortal network. This can be used for local blocking, or even for curating and sharing content lists. Lists are backed off to JSON files (in the lists folder) to ease sharing between nodes and users.

This first implementation allows access to an address blacklist only, but has been written in such a way that other lists can be easily added. This might be needed in the future, e.g. to blacklist a group, a poll, or some hosted data. It could also be used by community members to curate lists of favourite or problematic content, which could then be shared or even subscribed to on the chain by other users.
2021-08-07 10:20:14 +01:00
CalDescent
a253294890 Ensure frozen ATs are still executed every block.
We currently want to execute frozen ATs, to maintain backwards support. We could optionally choose to stop executing them later, via a hard fork.
2021-08-06 20:01:59 +01:00
CalDescent
ec008b4a16 Merge branch 'AT-sleep-until-message' 2021-08-04 19:00:24 +01:00
CalDescent
1d65e34fe5 Revert "Added DogecoinACCTv2 and DogecoinACCTv2TradeBot"
This reverts commit 797dff4752.
2021-08-04 18:59:36 +01:00
CalDescent
8ae78703ca Revert "Initial attempt at adding "sleep until message" functionality to DOGE ACCTv2."
This reverts commit a1c61a1146.
2021-08-04 18:59:30 +01:00
CalDescent
bd4b9a9fd3 Modified .gitignore to allow multiple testnets to exist by adding a number or other suffix. 2021-08-04 18:56:16 +01:00
CalDescent
f09677d376 Added inputs, outputs and feeAmount to /crosschain//walletbalance endpoints
The inputs and outputs contain a simpler version than the ones in the raw transaction, consisting of `address`, `amount`, and `addressInWallet`. The latter of the three is to know whether the address is one that is derived from the supplied xpub master public key.
2021-08-04 18:54:36 +01:00
CalDescent
f669e3f6c4 Fixed Dogecoin tests. 2021-08-04 18:48:59 +01:00
CalDescent
961c5ea962 Treat zero as null in sleepUntilHeight AT data. This is needed because we are unable to call setSleepUntilHeight() with a null value due to the datatype used in the CIYAM AT library. An alternate option would be to fork the AT library and use an Integer or Long rather than an int, but since we don't have a block zero, this is still a valid thing to check even when using that approach. 2021-08-04 09:22:17 +01:00
CalDescent
a1c61a1146 Initial attempt at adding "sleep until message" functionality to DOGE ACCTv2. 2021-08-02 20:08:53 +01:00
CalDescent
797dff4752 Added DogecoinACCTv2 and DogecoinACCTv2TradeBot 2021-08-02 20:07:34 +01:00
CalDescent
711ad638b8 Renamed Chinese translation files.
zh_SC renamed to zh_CN, and zh_TC renamed to zh_TW. This is necessary for the localization library to locate the files correctly.
2021-08-02 09:24:38 +01:00
CalDescent
4956c3328c Updated AdvancedInstaller project for v1.6.0 2021-08-01 19:14:55 +01:00
CalDescent
68190c8c76 Fixed a build error relating to using an int rather than Integer in the CIYAM AT library. Solved for now by using 0 instead of null, but will review this again before release. 2021-08-01 18:07:19 +01:00
CalDescent
dde47bc1fc Fixed build errors by adding sleepUntilMessageTimestamp to recent method additions. 2021-08-01 18:05:58 +01:00
CalDescent
744deaed8d Fixed merge issue due to differing db schemas. 2021-08-01 10:39:34 +01:00
CalDescent
a62910c8b6 Merge branch 'master' into AT-sleep-until-message 2021-08-01 10:30:52 +01:00
catbref
a9c7142d7b Speed up AT states reshape 2020-11-10 16:46:07 +00:00
catbref
7a40c3526f Bugfixes and tests for SLEEP_UNTIL_MESSAGE 2020-11-10 15:44:47 +00:00
catbref
3253d9d3fb WIP: initial implementation of AT sleep-until-message (untested) 2020-11-10 15:44:47 +00:00
26 changed files with 1567 additions and 60 deletions

5
.gitignore vendored
View File

@@ -1,4 +1,5 @@
/db*
/lists/
/bin/
/target/
/qortal-backup/
@@ -15,8 +16,8 @@
/settings.json
/testnet*
/settings*.json
/testchain.json
/run-testnet.sh
/testchain*.json
/run-testnet*.sh
/.idea
/qortal.iml
.DS_Store

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:{E67F58ED-2236-43A9-8895-B9AB96C60EB9} 1049:{E612D7B8-EB9F-481C-81FF-7530E0801F95} 2052:{8EC6F665-1D21-4DB8-8C55-05C2550FF1B3} 2057:{6CE725B6-BBDD-459D-8016-6D1D2FC1F4EC} " Type="16"/>
<ROW Property="ProductCode" Value="1033:{482E9390-1005-42FD-9F3F-E160E0E6FB19} 1049:{8FE09AC2-814B-42FC-9FCE-53D45A396529} 2052:{4FABD326-8345-438B-82B8-66C2DC3676E6} 2057:{7ECFFF43-DEC7-4B7F-BC88-260A10AF132A} " Type="16"/>
<ROW Property="ProductLanguage" Value="2057"/>
<ROW Property="ProductName" Value="Qortal"/>
<ROW Property="ProductVersion" Value="1.5.6" Type="32"/>
<ROW Property="ProductVersion" Value="1.6.0" 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="{9DA2985C-778C-4D85-A44E-5B00D935EED2}" Directory_="APPDIR" Attributes="260" KeyPath="DisplayName" Options="1"/>
<ROW Component="AI_CustomARPName" ComponentId="{7941AD6C-7C09-48E7-93ED-0340E0F52EC0}" 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

@@ -0,0 +1,18 @@
package org.qortal.api.model;
import io.swagger.v3.oas.annotations.media.Schema;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import java.util.List;
@XmlAccessorType(XmlAccessType.FIELD)
public class AddressListRequest {
@Schema(description = "A list of addresses")
public List<String> addresses;
public AddressListRequest() {
}
}

View File

@@ -0,0 +1,271 @@
package org.qortal.api.resource;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.qortal.api.*;
import org.qortal.api.model.AddressListRequest;
import org.qortal.crypto.Crypto;
import org.qortal.data.account.AccountData;
import org.qortal.list.ResourceListManager;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
@Path("/lists")
@Tag(name = "Lists")
public class ListsResource {
@Context
HttpServletRequest request;
@POST
@Path("/blacklist/address/{address}")
@Operation(
summary = "Add a QORT address to the local blacklist",
responses = {
@ApiResponse(
description = "Returns true on success, or an exception on failure",
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "boolean"))
)
}
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE})
public String addAddressToBlacklist(@PathParam("address") String address) {
if (!Crypto.isValidAddress(address))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
AccountData accountData = repository.getAccountRepository().getAccount(address);
// Not found?
if (accountData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
// Valid address, so go ahead and blacklist it
boolean success = ResourceListManager.getInstance().addAddressToBlacklist(address, true);
return success ? "true" : "false";
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@POST
@Path("/blacklist/addresses")
@Operation(
summary = "Add one or more QORT addresses to the local blacklist",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = AddressListRequest.class
)
)
),
responses = {
@ApiResponse(
description = "Returns true if all addresses were processed, false if any couldn't be " +
"processed, or an exception on failure. If false or an exception is returned, " +
"the list will not be updated, and the request will need to be re-issued.",
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "boolean"))
)
}
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE})
public String addAddressesToBlacklist(AddressListRequest addressListRequest) {
if (addressListRequest == null || addressListRequest.addresses == null) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
}
int successCount = 0;
int errorCount = 0;
try (final Repository repository = RepositoryManager.getRepository()) {
for (String address : addressListRequest.addresses) {
if (!Crypto.isValidAddress(address)) {
errorCount++;
continue;
}
AccountData accountData = repository.getAccountRepository().getAccount(address);
// Not found?
if (accountData == null) {
errorCount++;
continue;
}
// Valid address, so go ahead and blacklist it
boolean success = ResourceListManager.getInstance().addAddressToBlacklist(address, false);
if (success) {
successCount++;
}
else {
errorCount++;
}
}
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
if (successCount > 0 && errorCount == 0) {
// All were successful, so save the blacklist
ResourceListManager.getInstance().saveBlacklist();
return "true";
}
else {
// Something went wrong, so revert
ResourceListManager.getInstance().revertBlacklist();
return "false";
}
}
@DELETE
@Path("/blacklist/address/{address}")
@Operation(
summary = "Remove a QORT address from the local blacklist",
responses = {
@ApiResponse(
description = "Returns true on success, or an exception on failure",
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "boolean"))
)
}
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE})
public String removeAddressFromBlacklist(@PathParam("address") String address) {
if (!Crypto.isValidAddress(address))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
AccountData accountData = repository.getAccountRepository().getAccount(address);
// Not found?
if (accountData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
// Valid address, so go ahead and blacklist it
boolean success = ResourceListManager.getInstance().removeAddressFromBlacklist(address, true);
return success ? "true" : "false";
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
@DELETE
@Path("/blacklist/addresses")
@Operation(
summary = "Remove one or more QORT addresses from the local blacklist",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = AddressListRequest.class
)
)
),
responses = {
@ApiResponse(
description = "Returns true if all addresses were processed, false if any couldn't be " +
"processed, or an exception on failure. If false or an exception is returned, " +
"the list will not be updated, and the request will need to be re-issued.",
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "boolean"))
)
}
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE})
public String removeAddressesFromBlacklist(AddressListRequest addressListRequest) {
if (addressListRequest == null || addressListRequest.addresses == null) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
}
int successCount = 0;
int errorCount = 0;
try (final Repository repository = RepositoryManager.getRepository()) {
for (String address : addressListRequest.addresses) {
if (!Crypto.isValidAddress(address)) {
errorCount++;
continue;
}
AccountData accountData = repository.getAccountRepository().getAccount(address);
// Not found?
if (accountData == null) {
errorCount++;
continue;
}
// Valid address, so go ahead and blacklist it
// Don't save as we will do this at the end of the process
boolean success = ResourceListManager.getInstance().removeAddressFromBlacklist(address, false);
if (success) {
successCount++;
}
else {
errorCount++;
}
}
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
if (successCount > 0 && errorCount == 0) {
// All were successful, so save the blacklist
ResourceListManager.getInstance().saveBlacklist();
return "true";
}
else {
// Something went wrong, so revert
ResourceListManager.getInstance().revertBlacklist();
return "false";
}
}
@GET
@Path("/blacklist/address/{address}")
@Operation(
summary = "Check if an address is present in the local blacklist",
responses = {
@ApiResponse(
description = "Returns true or false if the list was queried, or an exception on failure",
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "boolean"))
)
}
)
@ApiErrors({ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE})
public String checkAddressInBlacklist(@PathParam("address") String address) {
if (!Crypto.isValidAddress(address))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
try (final Repository repository = RepositoryManager.getRepository()) {
AccountData accountData = repository.getAccountRepository().getAccount(address);
// Not found?
if (accountData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
// Valid address, so go ahead and blacklist it
boolean blacklisted = ResourceListManager.getInstance().isAddressInBlacklist(address);
return blacklisted ? "true" : "false";
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
}
}
}

View File

@@ -1,5 +1,7 @@
package org.qortal.at;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.ciyam.at.MachineState;
@@ -56,12 +58,12 @@ public class AT {
this.atData = new ATData(atAddress, creatorPublicKey, creation, machineState.version, assetId, codeBytes, codeHash,
machineState.isSleeping(), machineState.getSleepUntilHeight(), machineState.isFinished(), machineState.hadFatalError(),
machineState.isFrozen(), machineState.getFrozenBalance());
machineState.isFrozen(), machineState.getFrozenBalance(), null);
byte[] stateData = machineState.toBytes();
byte[] stateHash = Crypto.digest(stateData);
this.atStateData = new ATStateData(atAddress, height, stateData, stateHash, 0L, true);
this.atStateData = new ATStateData(atAddress, height, stateData, stateHash, 0L, true, null);
}
// Getters / setters
@@ -84,13 +86,28 @@ public class AT {
this.repository.getATRepository().delete(this.atData.getATAddress());
}
/**
* Potentially execute AT.
* <p>
* Note that sleep-until-message support might set/reset
* sleep-related flags/values.
* <p>
* {@link #getATStateData()} will return null if nothing happened.
* <p>
* @param blockHeight
* @param blockTimestamp
* @return AT-generated transactions, possibly empty
* @throws DataException
*/
public List<AtTransaction> run(int blockHeight, long blockTimestamp) throws DataException {
String atAddress = this.atData.getATAddress();
QortalATAPI api = new QortalATAPI(repository, this.atData, blockTimestamp);
QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance();
byte[] codeBytes = this.atData.getCodeBytes();
if (!api.willExecute(blockHeight))
// this.atStateData will be null
return Collections.emptyList();
// Fetch latest ATStateData for this AT
ATStateData latestAtStateData = this.repository.getATRepository().getLatestATState(atAddress);
@@ -100,8 +117,10 @@ public class AT {
throw new IllegalStateException("No previous AT state data found");
// [Re]create AT machine state using AT state data or from scratch as applicable
byte[] codeBytes = this.atData.getCodeBytes();
MachineState state = MachineState.fromBytes(api, loggerFactory, latestAtStateData.getStateData(), codeBytes);
try {
api.preExecute(state);
state.execute();
} catch (Exception e) {
throw new DataException(String.format("Uncaught exception while running AT '%s'", atAddress), e);
@@ -109,9 +128,18 @@ public class AT {
byte[] stateData = state.toBytes();
byte[] stateHash = Crypto.digest(stateData);
long atFees = api.calcFinalFees(state);
this.atStateData = new ATStateData(atAddress, blockHeight, stateData, stateHash, atFees, false);
// Nothing happened?
if (state.getSteps() == 0 && Arrays.equals(stateHash, latestAtStateData.getStateHash()))
// We currently want to execute frozen ATs, to maintain backwards support.
if (state.isFrozen() == false)
// this.atStateData will be null
return Collections.emptyList();
long atFees = api.calcFinalFees(state);
Long sleepUntilMessageTimestamp = this.atData.getSleepUntilMessageTimestamp();
this.atStateData = new ATStateData(atAddress, blockHeight, stateData, stateHash, atFees, false, sleepUntilMessageTimestamp);
return api.getTransactions();
}
@@ -130,6 +158,10 @@ public class AT {
this.atData.setHadFatalError(state.hadFatalError());
this.atData.setIsFrozen(state.isFrozen());
this.atData.setFrozenBalance(state.getFrozenBalance());
// Special sleep-until-message support
this.atData.setSleepUntilMessageTimestamp(this.atStateData.getSleepUntilMessageTimestamp());
this.repository.getATRepository().save(this.atData);
}
@@ -157,6 +189,10 @@ public class AT {
this.atData.setHadFatalError(state.hadFatalError());
this.atData.setIsFrozen(state.isFrozen());
this.atData.setFrozenBalance(state.getFrozenBalance());
// Special sleep-until-message support
this.atData.setSleepUntilMessageTimestamp(previousStateData.getSleepUntilMessageTimestamp());
this.repository.getATRepository().save(this.atData);
}

View File

@@ -32,6 +32,7 @@ import org.qortal.group.Group;
import org.qortal.repository.ATRepository;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.ATRepository.NextTransactionInfo;
import org.qortal.transaction.AtTransaction;
import org.qortal.transaction.Transaction.TransactionType;
import org.qortal.utils.Base58;
@@ -74,8 +75,45 @@ public class QortalATAPI extends API {
return this.transactions;
}
public long calcFinalFees(MachineState state) {
return state.getSteps() * this.ciyamAtSettings.feePerStep;
public boolean willExecute(int blockHeight) throws DataException {
// Sleep-until-message/height checking
Long sleepUntilMessageTimestamp = this.atData.getSleepUntilMessageTimestamp();
if (sleepUntilMessageTimestamp != null) {
// Quicker to check height, if sleep-until-height also active
Integer sleepUntilHeight = this.atData.getSleepUntilHeight();
boolean wakeDueToHeight = sleepUntilHeight != null && sleepUntilHeight != 0 && blockHeight >= sleepUntilHeight;
boolean wakeDueToMessage = false;
if (!wakeDueToHeight) {
// No avoiding asking repository
Timestamp previousTxTimestamp = new Timestamp(sleepUntilMessageTimestamp);
NextTransactionInfo nextTransactionInfo = this.repository.getATRepository().findNextTransaction(this.atData.getATAddress(),
previousTxTimestamp.blockHeight,
previousTxTimestamp.transactionSequence);
wakeDueToMessage = nextTransactionInfo != null;
}
// Can we skip?
if (!wakeDueToHeight && !wakeDueToMessage)
return false;
}
return true;
}
public void preExecute(MachineState state) {
// Sleep-until-message/height checking
Long sleepUntilMessageTimestamp = this.atData.getSleepUntilMessageTimestamp();
if (sleepUntilMessageTimestamp != null) {
// We've passed checks, so clear sleep-related flags/values
this.setIsSleeping(state, false);
this.setSleepUntilHeight(state, 0);
this.atData.setSleepUntilMessageTimestamp(null);
}
}
// Inherited methods from CIYAM AT API
@@ -412,6 +450,10 @@ public class QortalATAPI extends API {
// Utility methods
public long calcFinalFees(MachineState state) {
return state.getSteps() * this.ciyamAtSettings.feePerStep;
}
/** Returns partial transaction signature, used to verify we're operating on the same transaction and not naively using block height & sequence. */
public static byte[] partialSignature(byte[] fullSignature) {
return Arrays.copyOfRange(fullSignature, 8, 32);
@@ -460,6 +502,15 @@ public class QortalATAPI extends API {
}
}
/*package*/ void sleepUntilMessageOrHeight(MachineState state, long txTimestamp, Long sleepUntilHeight) {
this.setIsSleeping(state, true);
this.atData.setSleepUntilMessageTimestamp(txTimestamp);
if (sleepUntilHeight != null)
this.setSleepUntilHeight(state, sleepUntilHeight.intValue());
}
/** Returns AT's account */
/* package */ Account getATAccount() {
return new Account(this.repository, this.atData.getATAddress());

View File

@@ -84,6 +84,43 @@ public enum QortalFunctionCode {
api.setB(state, bBytes);
}
},
/**
* Sleep AT until a new message arrives after 'tx-timestamp'.<br>
* <tt>0x0503 tx-timestamp</tt>
*/
SLEEP_UNTIL_MESSAGE(0x0503, 1, false) {
@Override
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
if (functionData.value1 <= 0)
return;
long txTimestamp = functionData.value1;
QortalATAPI api = (QortalATAPI) state.getAPI();
api.sleepUntilMessageOrHeight(state, txTimestamp, null);
}
},
/**
* Sleep AT until a new message arrives, after 'tx-timestamp', or height reached.<br>
* <tt>0x0504 tx-timestamp height</tt>
*/
SLEEP_UNTIL_MESSAGE_OR_HEIGHT(0x0504, 2, false) {
@Override
protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
if (functionData.value1 <= 0)
return;
long txTimestamp = functionData.value1;
if (functionData.value2 <= 0)
return;
long sleepUntilHeight = functionData.value2;
QortalATAPI api = (QortalATAPI) state.getAPI();
api.sleepUntilMessageOrHeight(state, txTimestamp, sleepUntilHeight);
}
},
/**
* Convert address in B to 20-byte value in LSB of B1, and all of B2 & B3.<br>
* <tt>0x0510</tt>

View File

@@ -1247,12 +1247,13 @@ public class Block {
for (ATData atData : executableATs) {
AT at = new AT(this.repository, atData);
List<AtTransaction> atTransactions = at.run(this.blockData.getHeight(), this.blockData.getTimestamp());
ATStateData atStateData = at.getATStateData();
// Didn't execute? (e.g. sleeping)
if (atStateData == null)
continue;
allAtTransactions.addAll(atTransactions);
ATStateData atStateData = at.getATStateData();
this.ourAtStates.add(atStateData);
this.ourAtFees += atStateData.getFees();
}

View File

@@ -406,14 +406,24 @@ public abstract class Bitcoiny implements ForeignBlockchain {
protected SimpleTransaction convertToSimpleTransaction(BitcoinyTransaction t, Set<String> keySet) {
long amount = 0;
long total = 0L;
long totalInputAmount = 0L;
long totalOutputAmount = 0L;
List<SimpleTransaction.Input> inputs = new ArrayList<>();
List<SimpleTransaction.Output> outputs = new ArrayList<>();
for (BitcoinyTransaction.Input input : t.inputs) {
try {
BitcoinyTransaction t2 = getTransaction(input.outputTxHash);
List<String> senders = t2.outputs.get(input.outputVout).addresses;
long inputAmount = t2.outputs.get(input.outputVout).value;
totalInputAmount += inputAmount;
for (String sender : senders) {
boolean addressInWallet = false;
if (keySet.contains(sender)) {
total += t2.outputs.get(input.outputVout).value;
total += inputAmount;
addressInWallet = true;
}
inputs.add(new SimpleTransaction.Input(sender, inputAmount, addressInWallet));
}
} catch (ForeignBlockchainException e) {
LOGGER.trace("Failed to retrieve transaction information {}", input.outputTxHash);
@@ -422,17 +432,22 @@ public abstract class Bitcoiny implements ForeignBlockchain {
if (t.outputs != null && !t.outputs.isEmpty()) {
for (BitcoinyTransaction.Output output : t.outputs) {
for (String address : output.addresses) {
boolean addressInWallet = false;
if (keySet.contains(address)) {
if (total > 0L) {
amount -= (total - output.value);
} else {
amount += output.value;
}
addressInWallet = true;
}
outputs.add(new SimpleTransaction.Output(address, output.value, addressInWallet));
}
totalOutputAmount += output.value;
}
}
return new SimpleTransaction(t.txHash, t.timestamp, amount);
long fee = totalInputAmount - totalOutputAmount;
return new SimpleTransaction(t.txHash, t.timestamp, amount, fee, inputs, outputs);
}
/**

View File

@@ -2,20 +2,85 @@ package org.qortal.crosschain;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import java.util.List;
@XmlAccessorType(XmlAccessType.FIELD)
public class SimpleTransaction {
private String txHash;
private Integer timestamp;
private long totalAmount;
private long feeAmount;
private List<Input> inputs;
private List<Output> outputs;
@XmlAccessorType(XmlAccessType.FIELD)
public static class Input {
private String address;
private long amount;
private boolean addressInWallet;
public Input() {
}
public Input(String address, long amount, boolean addressInWallet) {
this.address = address;
this.amount = amount;
this.addressInWallet = addressInWallet;
}
public String getAddress() {
return address;
}
public long getAmount() {
return amount;
}
public boolean getAddressInWallet() {
return addressInWallet;
}
}
@XmlAccessorType(XmlAccessType.FIELD)
public static class Output {
private String address;
private long amount;
private boolean addressInWallet;
public Output() {
}
public Output(String address, long amount, boolean addressInWallet) {
this.address = address;
this.amount = amount;
this.addressInWallet = addressInWallet;
}
public String getAddress() {
return address;
}
public long getAmount() {
return amount;
}
public boolean getAddressInWallet() {
return addressInWallet;
}
}
public SimpleTransaction() {
}
public SimpleTransaction(String txHash, Integer timestamp, long totalAmount) {
public SimpleTransaction(String txHash, Integer timestamp, long totalAmount, long feeAmount, List<Input> inputs, List<Output> outputs) {
this.txHash = txHash;
this.timestamp = timestamp;
this.totalAmount = totalAmount;
this.feeAmount = feeAmount;
this.inputs = inputs;
this.outputs = outputs;
}
public String getTxHash() {
@@ -29,4 +94,16 @@ public class SimpleTransaction {
public long getTotalAmount() {
return totalAmount;
}
}
public long getFeeAmount() {
return feeAmount;
}
public List<Input> getInputs() {
return this.inputs;
}
public List<Output> getOutputs() {
return this.outputs;
}
}

View File

@@ -23,6 +23,7 @@ public class ATData {
private boolean isFrozen;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private Long frozenBalance;
private Long sleepUntilMessageTimestamp;
// Constructors
@@ -31,7 +32,8 @@ public class ATData {
}
public ATData(String ATAddress, byte[] creatorPublicKey, long creation, int version, long assetId, byte[] codeBytes, byte[] codeHash,
boolean isSleeping, Integer sleepUntilHeight, boolean isFinished, boolean hadFatalError, boolean isFrozen, Long frozenBalance) {
boolean isSleeping, Integer sleepUntilHeight, boolean isFinished, boolean hadFatalError, boolean isFrozen, Long frozenBalance,
Long sleepUntilMessageTimestamp) {
this.ATAddress = ATAddress;
this.creatorPublicKey = creatorPublicKey;
this.creation = creation;
@@ -45,6 +47,7 @@ public class ATData {
this.hadFatalError = hadFatalError;
this.isFrozen = isFrozen;
this.frozenBalance = frozenBalance;
this.sleepUntilMessageTimestamp = sleepUntilMessageTimestamp;
}
/** For constructing skeleton ATData with bare minimum info. */
@@ -133,4 +136,12 @@ public class ATData {
this.frozenBalance = frozenBalance;
}
public Long getSleepUntilMessageTimestamp() {
return this.sleepUntilMessageTimestamp;
}
public void setSleepUntilMessageTimestamp(Long sleepUntilMessageTimestamp) {
this.sleepUntilMessageTimestamp = sleepUntilMessageTimestamp;
}
}

View File

@@ -10,35 +10,32 @@ public class ATStateData {
private Long fees;
private boolean isInitial;
// Qortal-AT-specific
private Long sleepUntilMessageTimestamp;
// Constructors
/** Create new ATStateData */
public ATStateData(String ATAddress, Integer height, byte[] stateData, byte[] stateHash, Long fees, boolean isInitial) {
public ATStateData(String ATAddress, Integer height, byte[] stateData, byte[] stateHash, Long fees,
boolean isInitial, Long sleepUntilMessageTimestamp) {
this.ATAddress = ATAddress;
this.height = height;
this.stateData = stateData;
this.stateHash = stateHash;
this.fees = fees;
this.isInitial = isInitial;
this.sleepUntilMessageTimestamp = sleepUntilMessageTimestamp;
}
/** For recreating per-block ATStateData from repository where not all info is needed */
public ATStateData(String ATAddress, int height, byte[] stateHash, Long fees, boolean isInitial) {
this(ATAddress, height, null, stateHash, fees, isInitial);
}
/** For creating ATStateData from serialized bytes when we don't have all the info */
public ATStateData(String ATAddress, byte[] stateHash) {
// This won't ever be initial AT state from deployment as that's never serialized over the network,
// but generated when the DeployAtTransaction is processed locally.
this(ATAddress, null, null, stateHash, null, false);
this(ATAddress, height, null, stateHash, fees, isInitial, null);
}
/** For creating ATStateData from serialized bytes when we don't have all the info */
public ATStateData(String ATAddress, byte[] stateHash, Long fees) {
// This won't ever be initial AT state from deployment as that's never serialized over the network,
// but generated when the DeployAtTransaction is processed locally.
this(ATAddress, null, null, stateHash, fees, false);
// This won't ever be initial AT state from deployment, as that's never serialized over the network.
this(ATAddress, null, null, stateHash, fees, false, null);
}
// Getters / setters
@@ -72,4 +69,12 @@ public class ATStateData {
return this.isInitial;
}
public Long getSleepUntilMessageTimestamp() {
return this.sleepUntilMessageTimestamp;
}
public void setSleepUntilMessageTimestamp(Long sleepUntilMessageTimestamp) {
this.sleepUntilMessageTimestamp = sleepUntilMessageTimestamp;
}
}

View File

@@ -0,0 +1,137 @@
package org.qortal.list;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.json.JSONArray;
import org.qortal.settings.Settings;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
public class ResourceList {
private static final Logger LOGGER = LogManager.getLogger(ResourceList.class);
private String category;
private String resourceName;
private List<String> list;
/**
* ResourceList
* Creates or updates a list for the purpose of tracking resources on the Qortal network
* This can be used for local blocking, or even for curating and sharing content lists
* Lists are backed off to JSON files (in the lists folder) to ease sharing between nodes and users
*
* @param category - for instance "blacklist", "whitelist", or "userlist"
* @param resourceName - for instance "address", "poll", or "group"
* @throws IOException
*/
public ResourceList(String category, String resourceName) throws IOException {
this.category = category;
this.resourceName = resourceName;
this.load();
}
/* Filesystem */
private Path getFilePath() {
String pathString = String.format("%s%s%s_%s.json", Settings.getInstance().getListsPath(),
File.separator, this.resourceName, this.category);
Path outputFilePath = Paths.get(pathString);
try {
Files.createDirectories(outputFilePath.getParent());
} catch (IOException e) {
throw new IllegalStateException("Unable to create lists directory");
}
return outputFilePath;
}
public void save() throws IOException {
if (this.resourceName == null) {
throw new IllegalStateException("Can't save list with missing resource name");
}
if (this.category == null) {
throw new IllegalStateException("Can't save list with missing category");
}
String jsonString = ResourceList.listToJSONString(this.list);
Path filePath = this.getFilePath();
BufferedWriter writer = new BufferedWriter(new FileWriter(filePath.toString()));
writer.write(jsonString);
writer.close();
}
private boolean load() throws IOException {
Path path = this.getFilePath();
File resourceListFile = new File(path.toString());
if (!resourceListFile.exists()) {
return false;
}
try {
String jsonString = new String(Files.readAllBytes(path));
this.list = ResourceList.listFromJSONString(jsonString);
} catch (IOException e) {
throw new IOException(String.format("Couldn't read contents from file %s", path.toString()));
}
return true;
}
public boolean revert() {
try {
return this.load();
} catch (IOException e) {
LOGGER.info("Unable to revert {} {}", this.resourceName, this.category);
}
return false;
}
/* List management */
public void add(String resource) {
if (!this.contains(resource)) {
this.list.add(resource);
}
}
public void remove(String resource) {
this.list.remove(resource);
}
public boolean contains(String resource) {
return this.list.contains(resource);
}
/* Utils */
public static String listToJSONString(List<String> list) {
JSONArray items = new JSONArray();
for (String item : list) {
items.put(item);
}
return items.toString(4);
}
private static List<String> listFromJSONString(String jsonString) {
JSONArray jsonList = new JSONArray(jsonString);
List<String> resourceList = new ArrayList<>();
for (int i=0; i<jsonList.length(); i++) {
String item = (String)jsonList.get(i);
resourceList.add(item);
}
return resourceList;
}
}

View File

@@ -0,0 +1,87 @@
package org.qortal.list;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
public class ResourceListManager {
private static final Logger LOGGER = LogManager.getLogger(ResourceListManager.class);
private static ResourceListManager instance;
private ResourceList addressBlacklist;
public ResourceListManager() {
try {
this.addressBlacklist = new ResourceList("blacklist", "address");
} catch (IOException e) {
LOGGER.info("Error while loading address blacklist. Blocking is currently unavailable.");
}
}
public static synchronized ResourceListManager getInstance() {
if (instance == null) {
instance = new ResourceListManager();
}
return instance;
}
public boolean addAddressToBlacklist(String address, boolean save) {
try {
this.addressBlacklist.add(address);
if (save) {
this.addressBlacklist.save();
}
return true;
} catch (IllegalStateException | IOException e) {
LOGGER.info("Unable to add address to blacklist", e);
return false;
}
}
public boolean removeAddressFromBlacklist(String address, boolean save) {
try {
this.addressBlacklist.remove(address);
if (save) {
this.addressBlacklist.save();
}
return true;
} catch (IllegalStateException | IOException e) {
LOGGER.info("Unable to remove address from blacklist", e);
return false;
}
}
public boolean isAddressInBlacklist(String address) {
if (this.addressBlacklist == null) {
return false;
}
return this.addressBlacklist.contains(address);
}
public void saveBlacklist() {
if (this.addressBlacklist == null) {
return;
}
try {
this.addressBlacklist.save();
} catch (IOException e) {
LOGGER.info("Unable to save blacklist - reverting back to last saved state");
this.addressBlacklist.revert();
}
}
public void revertBlacklist() {
if (this.addressBlacklist == null) {
return;
}
this.addressBlacklist.revert();
}
}

View File

@@ -103,7 +103,7 @@ public interface ATRepository {
/**
* Returns all ATStateData for a given block height.
* <p>
* Unlike <tt>getATState</tt>, only returns ATStateData saved at the given height.
* Unlike <tt>getATState</tt>, only returns <i>partial</i> ATStateData saved at the given height.
*
* @param height
* - block height

View File

@@ -32,7 +32,7 @@ public class HSQLDBATRepository implements ATRepository {
public ATData fromATAddress(String atAddress) throws DataException {
String sql = "SELECT creator, created_when, version, asset_id, code_bytes, code_hash, "
+ "is_sleeping, sleep_until_height, is_finished, had_fatal_error, "
+ "is_frozen, frozen_balance "
+ "is_frozen, frozen_balance, sleep_until_message_timestamp "
+ "FROM ATs "
+ "WHERE AT_address = ? LIMIT 1";
@@ -60,8 +60,13 @@ public class HSQLDBATRepository implements ATRepository {
if (frozenBalance == 0 && resultSet.wasNull())
frozenBalance = null;
Long sleepUntilMessageTimestamp = resultSet.getLong(13);
if (sleepUntilMessageTimestamp == 0 && resultSet.wasNull())
sleepUntilMessageTimestamp = null;
return new ATData(atAddress, creatorPublicKey, created, version, assetId, codeBytes, codeHash,
isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance);
isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance,
sleepUntilMessageTimestamp);
} catch (SQLException e) {
throw new DataException("Unable to fetch AT from repository", e);
}
@@ -94,7 +99,7 @@ public class HSQLDBATRepository implements ATRepository {
public List<ATData> getAllExecutableATs() throws DataException {
String sql = "SELECT AT_address, creator, created_when, version, asset_id, code_bytes, code_hash, "
+ "is_sleeping, sleep_until_height, had_fatal_error, "
+ "is_frozen, frozen_balance "
+ "is_frozen, frozen_balance, sleep_until_message_timestamp "
+ "FROM ATs "
+ "WHERE is_finished = false "
+ "ORDER BY created_when ASC";
@@ -128,8 +133,13 @@ public class HSQLDBATRepository implements ATRepository {
if (frozenBalance == 0 && resultSet.wasNull())
frozenBalance = null;
Long sleepUntilMessageTimestamp = resultSet.getLong(13);
if (sleepUntilMessageTimestamp == 0 && resultSet.wasNull())
sleepUntilMessageTimestamp = null;
ATData atData = new ATData(atAddress, creatorPublicKey, created, version, assetId, codeBytes, codeHash,
isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance);
isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance,
sleepUntilMessageTimestamp);
executableATs.add(atData);
} while (resultSet.next());
@@ -147,7 +157,7 @@ public class HSQLDBATRepository implements ATRepository {
sql.append("SELECT AT_address, creator, created_when, version, asset_id, code_bytes, ")
.append("is_sleeping, sleep_until_height, is_finished, had_fatal_error, ")
.append("is_frozen, frozen_balance ")
.append("is_frozen, frozen_balance, sleep_until_message_timestamp ")
.append("FROM ATs ")
.append("WHERE code_hash = ? ");
bindParams.add(codeHash);
@@ -191,8 +201,13 @@ public class HSQLDBATRepository implements ATRepository {
if (frozenBalance == 0 && resultSet.wasNull())
frozenBalance = null;
Long sleepUntilMessageTimestamp = resultSet.getLong(13);
if (sleepUntilMessageTimestamp == 0 && resultSet.wasNull())
sleepUntilMessageTimestamp = null;
ATData atData = new ATData(atAddress, creatorPublicKey, created, version, assetId, codeBytes, codeHash,
isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance);
isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance,
sleepUntilMessageTimestamp);
matchingATs.add(atData);
} while (resultSet.next());
@@ -210,7 +225,7 @@ public class HSQLDBATRepository implements ATRepository {
sql.append("SELECT AT_address, creator, created_when, version, asset_id, code_bytes, ")
.append("is_sleeping, sleep_until_height, is_finished, had_fatal_error, ")
.append("is_frozen, frozen_balance, code_hash ")
.append("is_frozen, frozen_balance, code_hash, sleep_until_message_timestamp ")
.append("FROM ");
// (VALUES (?), (?), ...) AS ATCodeHashes (code_hash)
@@ -264,9 +279,10 @@ public class HSQLDBATRepository implements ATRepository {
frozenBalance = null;
byte[] codeHash = resultSet.getBytes(13);
Long sleepUntilMessageTimestamp = resultSet.getLong(14);
ATData atData = new ATData(atAddress, creatorPublicKey, created, version, assetId, codeBytes, codeHash,
isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance);
isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance, sleepUntilMessageTimestamp);
matchingATs.add(atData);
} while (resultSet.next());
@@ -305,7 +321,7 @@ public class HSQLDBATRepository implements ATRepository {
.bind("code_bytes", atData.getCodeBytes()).bind("code_hash", atData.getCodeHash())
.bind("is_sleeping", atData.getIsSleeping()).bind("sleep_until_height", atData.getSleepUntilHeight())
.bind("is_finished", atData.getIsFinished()).bind("had_fatal_error", atData.getHadFatalError()).bind("is_frozen", atData.getIsFrozen())
.bind("frozen_balance", atData.getFrozenBalance());
.bind("frozen_balance", atData.getFrozenBalance()).bind("sleep_until_message_timestamp", atData.getSleepUntilMessageTimestamp());
try {
saveHelper.execute(this.repository);
@@ -328,7 +344,7 @@ public class HSQLDBATRepository implements ATRepository {
@Override
public ATStateData getATStateAtHeight(String atAddress, int height) throws DataException {
String sql = "SELECT state_data, state_hash, fees, is_initial "
String sql = "SELECT state_data, state_hash, fees, is_initial, sleep_until_message_timestamp "
+ "FROM ATStates "
+ "LEFT OUTER JOIN ATStatesData USING (AT_address, height) "
+ "WHERE ATStates.AT_address = ? AND ATStates.height = ? "
@@ -343,7 +359,11 @@ public class HSQLDBATRepository implements ATRepository {
long fees = resultSet.getLong(3);
boolean isInitial = resultSet.getBoolean(4);
return new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial);
Long sleepUntilMessageTimestamp = resultSet.getLong(5);
if (sleepUntilMessageTimestamp == 0 && resultSet.wasNull())
sleepUntilMessageTimestamp = null;
return new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial, sleepUntilMessageTimestamp);
} catch (SQLException e) {
throw new DataException("Unable to fetch AT state from repository", e);
}
@@ -351,7 +371,7 @@ public class HSQLDBATRepository implements ATRepository {
@Override
public ATStateData getLatestATState(String atAddress) throws DataException {
String sql = "SELECT height, state_data, state_hash, fees, is_initial "
String sql = "SELECT height, state_data, state_hash, fees, is_initial, sleep_until_message_timestamp "
+ "FROM ATStates "
+ "JOIN ATStatesData USING (AT_address, height) "
+ "WHERE ATStates.AT_address = ? "
@@ -370,7 +390,11 @@ public class HSQLDBATRepository implements ATRepository {
long fees = resultSet.getLong(4);
boolean isInitial = resultSet.getBoolean(5);
return new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial);
Long sleepUntilMessageTimestamp = resultSet.getLong(6);
if (sleepUntilMessageTimestamp == 0 && resultSet.wasNull())
sleepUntilMessageTimestamp = null;
return new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial, sleepUntilMessageTimestamp);
} catch (SQLException e) {
throw new DataException("Unable to fetch latest AT state from repository", e);
}
@@ -383,10 +407,10 @@ public class HSQLDBATRepository implements ATRepository {
StringBuilder sql = new StringBuilder(1024);
List<Object> bindParams = new ArrayList<>();
sql.append("SELECT AT_address, height, state_data, state_hash, fees, is_initial "
sql.append("SELECT AT_address, height, state_data, state_hash, fees, is_initial, FinalATStates.sleep_until_message_timestamp "
+ "FROM ATs "
+ "CROSS JOIN LATERAL("
+ "SELECT height, state_data, state_hash, fees, is_initial "
+ "SELECT height, state_data, state_hash, fees, is_initial, sleep_until_message_timestamp "
+ "FROM ATStates "
+ "JOIN ATStatesData USING (AT_address, height) "
+ "WHERE ATStates.AT_address = ATs.AT_address ");
@@ -440,7 +464,11 @@ public class HSQLDBATRepository implements ATRepository {
long fees = resultSet.getLong(5);
boolean isInitial = resultSet.getBoolean(6);
ATStateData atStateData = new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial);
Long sleepUntilMessageTimestamp = resultSet.getLong(7);
if (sleepUntilMessageTimestamp == 0 && resultSet.wasNull())
sleepUntilMessageTimestamp = null;
ATStateData atStateData = new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial, sleepUntilMessageTimestamp);
atStates.add(atStateData);
} while (resultSet.next());
@@ -471,7 +499,7 @@ public class HSQLDBATRepository implements ATRepository {
StringBuilder sql = new StringBuilder(1024);
List<Object> bindParams = new ArrayList<>();
sql.append("SELECT AT_address, height, state_data, state_hash, fees, is_initial "
sql.append("SELECT AT_address, height, state_data, state_hash, fees, is_initial, sleep_until_message_timestamp "
+ "FROM ATs "
+ "CROSS JOIN LATERAL("
+ "SELECT height, state_data, state_hash, fees, is_initial "
@@ -526,8 +554,10 @@ public class HSQLDBATRepository implements ATRepository {
byte[] stateHash = resultSet.getBytes(4);
long fees = resultSet.getLong(5);
boolean isInitial = resultSet.getBoolean(6);
Long sleepUntilMessageTimestamp = resultSet.getLong(7);
ATStateData atStateData = new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial);
ATStateData atStateData = new ATStateData(atAddress, height, stateData, stateHash, fees, isInitial,
sleepUntilMessageTimestamp);
atStates.add(atStateData);
} while (resultSet.next());
@@ -662,7 +692,8 @@ public class HSQLDBATRepository implements ATRepository {
atStatesSaver.bind("AT_address", atStateData.getATAddress()).bind("height", atStateData.getHeight())
.bind("state_hash", atStateData.getStateHash())
.bind("fees", atStateData.getFees()).bind("is_initial", atStateData.isInitial());
.bind("fees", atStateData.getFees()).bind("is_initial", atStateData.isInitial())
.bind("sleep_until_message_timestamp", atStateData.getSleepUntilMessageTimestamp());
try {
atStatesSaver.execute(this.repository);

View File

@@ -699,7 +699,7 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("CHECKPOINT");
break;
case 30:
case 30: {
// Split AT state data off to new table for better performance/management.
if (!wasPristine && !"mem".equals(HSQLDBRepository.getDbPathname(connection.getMetaData().getURL()))) {
@@ -774,6 +774,7 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("ALTER TABLE ATStatesNew RENAME TO ATStates");
stmt.execute("CHECKPOINT");
break;
}
case 31:
// Fix latest AT state cache which was previous created as TEMPORARY
@@ -822,6 +823,41 @@ public class HSQLDBDatabaseUpdates {
+ "timestamp_signature Signature NOT NULL, " + TRANSACTION_KEYS + ")");
break;
case 34: {
// AT sleep-until-message support
LOGGER.info("Altering AT table in repository - this might take a while... (approx. 20 seconds on high-spec)");
stmt.execute("ALTER TABLE ATs ADD sleep_until_message_timestamp BIGINT");
// Create new AT-states table with new column
stmt.execute("CREATE TABLE ATStatesNew ("
+ "AT_address QortalAddress, height INTEGER NOT NULL, state_hash ATStateHash NOT NULL, "
+ "fees QortalAmount NOT NULL, is_initial BOOLEAN NOT NULL, sleep_until_message_timestamp BIGINT, "
+ "PRIMARY KEY (AT_address, height), "
+ "FOREIGN KEY (AT_address) REFERENCES ATs (AT_address) ON DELETE CASCADE)");
stmt.execute("SET TABLE ATStatesNew NEW SPACE");
stmt.execute("CHECKPOINT");
ResultSet resultSet = stmt.executeQuery("SELECT height FROM Blocks ORDER BY height DESC LIMIT 1");
final int blockchainHeight = resultSet.next() ? resultSet.getInt(1) : 0;
final int heightStep = 100;
LOGGER.info("Altering AT states table in repository - this might take a while... (approx. 3 mins on high-spec)");
for (int minHeight = 1; minHeight < blockchainHeight; minHeight += heightStep) {
stmt.execute("INSERT INTO ATStatesNew ("
+ "SELECT AT_address, height, state_hash, fees, is_initial, NULL "
+ "FROM ATStates "
+ "WHERE height BETWEEN " + minHeight + " AND " + (minHeight + heightStep - 1)
+ ")");
stmt.execute("COMMIT");
}
stmt.execute("CHECKPOINT");
stmt.execute("DROP TABLE ATStates");
stmt.execute("ALTER TABLE ATStatesNew RENAME TO ATStates");
stmt.execute("CHECKPOINT");
break;
}
default:
// nothing to do
return false;

View File

@@ -161,6 +161,9 @@ public class Settings {
"https://raw.githubusercontent.com@151.101.16.133/Qortal/qortal/%s/qortal.update"
};
// Lists
private String listsPath = "lists";
/** Array of NTP server hostnames. */
private String[] ntpServers = new String[] {
"pool.ntp.org",
@@ -474,6 +477,10 @@ public class Settings {
return this.autoUpdateRepos;
}
public String getListsPath() {
return this.listsPath;
}
public String[] getNtpServers() {
return this.ntpServers;
}

View File

@@ -11,6 +11,7 @@ import org.qortal.crypto.MemoryPoW;
import org.qortal.data.transaction.ChatTransactionData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.group.Group;
import org.qortal.list.ResourceListManager;
import org.qortal.repository.DataException;
import org.qortal.repository.GroupRepository;
import org.qortal.repository.Repository;
@@ -138,6 +139,12 @@ public class ChatTransaction extends Transaction {
public ValidationResult isValid() throws DataException {
// Nonce checking is done via isSignatureValid() as that method is only called once per import
// Check for blacklisted author by address
ResourceListManager listManager = ResourceListManager.getInstance();
if (listManager.isAddressInBlacklist(this.chatTransactionData.getSender())) {
return ValidationResult.ADDRESS_IN_BLACKLIST;
}
// If we exist in the repository then we've been imported as unconfirmed,
// but we don't want to make it into a block, so return fake non-OK result.
if (this.repository.getTransactionRepository().exists(this.chatTransactionData.getSignature()))

View File

@@ -247,6 +247,7 @@ public abstract class Transaction {
INVALID_GROUP_BLOCK_DELAY(93),
INCORRECT_NONCE(94),
INVALID_TIMESTAMP_SIGNATURE(95),
ADDRESS_IN_BLACKLIST(96),
INVALID_BUT_OK(999),
NOT_YET_RELEASED(1000);

View File

@@ -354,7 +354,8 @@ public class AtRepositoryTests extends Common {
/*StateData*/ null,
atStateData.getStateHash(),
atStateData.getFees(),
atStateData.isInitial());
atStateData.isInitial(),
atStateData.getSleepUntilMessageTimestamp());
repository.getATRepository().save(newAtStateData);
atStateData = repository.getATRepository().getATStateAtHeight(atAddress, testHeight);

View File

@@ -0,0 +1,365 @@
package org.qortal.test.at;
import static org.junit.Assert.*;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
import org.ciyam.at.CompilationException;
import org.ciyam.at.FunctionCode;
import org.ciyam.at.MachineState;
import org.ciyam.at.OpCode;
import org.ciyam.at.Timestamp;
import org.junit.After;
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.at.QortalFunctionCode;
import org.qortal.block.Block;
import org.qortal.data.at.ATStateData;
import org.qortal.data.block.BlockData;
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.transaction.Transaction;
import org.qortal.utils.BitTwiddling;
public class SleepUntilMessageOrHeightTests extends Common {
private static final byte[] messageData = new byte[] { 0x44 };
private static final byte[] creationBytes = buildSleepUntilMessageOrHeightAT();
private static final long fundingAmount = 1_00000000L;
private static final long WAKE_HEIGHT = 10L;
private Repository repository = null;
private PrivateKeyAccount deployer;
private DeployAtTransaction deployAtTransaction;
private Account atAccount;
private String atAddress;
private byte[] rawNextTimestamp = new byte[32];
private Transaction transaction;
@Before
public void before() throws DataException {
Common.useDefaultSettings();
this.repository = RepositoryManager.getRepository();
this.deployer = Common.getTestAccount(repository, "alice");
this.deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
this.atAccount = deployAtTransaction.getATAccount();
this.atAddress = deployAtTransaction.getATAccount().getAddress();
}
@After
public void after() throws DataException {
if (this.repository != null)
this.repository.close();
this.repository = null;
}
@Test
public void testDeploy() throws DataException {
// Confirm initial value is zero
extractNextTxTimestamp(repository, atAddress, rawNextTimestamp);
assertArrayEquals(new byte[32], rawNextTimestamp);
}
@Test
public void testFeelessSleep() throws DataException {
// Mint block to allow AT to initialize and call SLEEP_UNTIL_MESSAGE
BlockUtils.mintBlock(repository);
// Fetch AT's balance for this height
long preMintBalance = atAccount.getConfirmedBalance(Asset.QORT);
// Mint block
BlockUtils.mintBlock(repository);
// Fetch new AT balance
long postMintBalance = atAccount.getConfirmedBalance(Asset.QORT);
assertEquals(preMintBalance, postMintBalance);
}
@Test
public void testFeelessSleep2() throws DataException {
// Mint block to allow AT to initialize and call SLEEP_UNTIL_MESSAGE
BlockUtils.mintBlock(repository);
// Fetch AT's balance for this height
long preMintBalance = atAccount.getConfirmedBalance(Asset.QORT);
// Mint several blocks
for (int i = 0; i < 5; ++i)
BlockUtils.mintBlock(repository);
// Fetch new AT balance
long postMintBalance = atAccount.getConfirmedBalance(Asset.QORT);
assertEquals(preMintBalance, postMintBalance);
}
@Test
public void testSleepUntilMessage() throws DataException {
// Mint block to allow AT to initialize and call SLEEP_UNTIL_MESSAGE_OR_HEIGHT
BlockUtils.mintBlock(repository);
// Send message to AT
transaction = sendMessage(repository, deployer, messageData, atAddress);
BlockUtils.mintBlock(repository);
// Mint block so AT executes and finds message
BlockUtils.mintBlock(repository);
// Confirm AT finds message
assertTimestamp(repository, atAddress, transaction);
}
@Test
public void testSleepUntilHeight() throws DataException {
// AT deployment in block 2
// Mint block to allow AT to initialize and call SLEEP_UNTIL_MESSAGE_OR_HEIGHT
BlockUtils.mintBlock(repository); // height now 3
// Fetch AT's balance for this height
long preMintBalance = atAccount.getConfirmedBalance(Asset.QORT);
// Mint several blocks
for (int i = 3; i < WAKE_HEIGHT; ++i)
BlockUtils.mintBlock(repository);
// We should now be at WAKE_HEIGHT
long height = repository.getBlockRepository().getBlockchainHeight();
assertEquals(WAKE_HEIGHT, height);
// AT should have woken and run at this height so balance should have changed
// Fetch new AT balance
long postMintBalance = atAccount.getConfirmedBalance(Asset.QORT);
assertNotSame(preMintBalance, postMintBalance);
// Confirm AT has no message
extractNextTxTimestamp(repository, atAddress, rawNextTimestamp);
assertArrayEquals(new byte[32], rawNextTimestamp);
// Mint yet another block
BlockUtils.mintBlock(repository);
// AT should also have woken and run at this height so balance should have changed
// Fetch new AT balance
long postMint2Balance = atAccount.getConfirmedBalance(Asset.QORT);
assertNotSame(postMintBalance, postMint2Balance);
// Confirm AT still has no message
extractNextTxTimestamp(repository, atAddress, rawNextTimestamp);
assertArrayEquals(new byte[32], rawNextTimestamp);
}
private static byte[] buildSleepUntilMessageOrHeightAT() {
// Labels for data segment addresses
int addrCounter = 0;
// Beginning of data segment for easy extraction
final int addrNextTx = addrCounter;
addrCounter += 4;
final int addrNextTxIndex = addrCounter++;
final int addrLastTxTimestamp = addrCounter++;
final int addrWakeHeight = addrCounter++;
// Data segment
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
// skip addrNextTx
dataByteBuffer.position(dataByteBuffer.position() + 4 * MachineState.VALUE_SIZE);
// Store pointer to addrNextTx at addrNextTxIndex
dataByteBuffer.putLong(addrNextTx);
// skip addrLastTxTimestamp
dataByteBuffer.position(dataByteBuffer.position() + MachineState.VALUE_SIZE);
// Store fixed wake height (block 10)
dataByteBuffer.putLong(WAKE_HEIGHT);
ByteBuffer codeByteBuffer = ByteBuffer.allocate(512);
// 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, addrLastTxTimestamp));
// Set restart position to after this opcode
codeByteBuffer.put(OpCode.SET_PCS.compile());
/* Loop, waiting for message to AT */
/* Sleep until message arrives */
codeByteBuffer.put(OpCode.EXT_FUN_DAT_2.compile(QortalFunctionCode.SLEEP_UNTIL_MESSAGE_OR_HEIGHT.value, addrLastTxTimestamp, addrWakeHeight));
// 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, addrLastTxTimestamp));
// Copy A to data segment, starting at addrNextTx (as pointed to by addrNextTxIndex)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_A_IND, addrNextTxIndex));
// Stop if timestamp part of A is zero
codeByteBuffer.put(OpCode.STZ_DAT.compile(addrNextTx));
// 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, addrLastTxTimestamp));
// We're done
codeByteBuffer.put(OpCode.FIN_IMD.compile());
} catch (CompilationException e) {
throw new IllegalStateException("Unable to compile AT?", e);
}
}
codeByteBuffer.flip();
byte[] codeBytes = new byte[codeByteBuffer.limit()];
codeByteBuffer.get(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);
}
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, byte[] creationBytes, long fundingAmount) throws DataException {
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 = "Test AT";
String description = "Test AT";
String atType = "Test";
String tags = "TEST";
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 void extractNextTxTimestamp(Repository repository, String atAddress, byte[] rawNextTimestamp) throws DataException {
// Check AT result
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
byte[] stateData = atStateData.getStateData();
byte[] dataBytes = MachineState.extractDataBytes(stateData);
System.arraycopy(dataBytes, 0, rawNextTimestamp, 0, rawNextTimestamp.length);
}
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.signAndImportValid(repository, messageTransactionData, sender);
return messageTransaction;
}
private void assertTimestamp(Repository repository, String atAddress, Transaction transaction) throws DataException {
int height = transaction.getHeight();
byte[] transactionSignature = transaction.getTransactionData().getSignature();
BlockData blockData = repository.getBlockRepository().fromHeight(height);
assertNotNull(blockData);
Block block = new Block(repository, blockData);
List<Transaction> blockTransactions = block.getTransactions();
int sequence;
for (sequence = blockTransactions.size() - 1; sequence >= 0; --sequence)
if (Arrays.equals(blockTransactions.get(sequence).getTransactionData().getSignature(), transactionSignature))
break;
assertNotSame(-1, sequence);
byte[] rawNextTimestamp = new byte[32];
extractNextTxTimestamp(repository, atAddress, rawNextTimestamp);
Timestamp expectedTimestamp = new Timestamp(height, sequence);
Timestamp actualTimestamp = new Timestamp(BitTwiddling.longFromBEBytes(rawNextTimestamp, 0));
assertEquals(String.format("Expected height %d, seq %d but was height %d, seq %d",
height, sequence,
actualTimestamp.blockHeight, actualTimestamp.transactionSequence
),
expectedTimestamp.longValue(),
actualTimestamp.longValue());
byte[] expectedPartialSignature = new byte[24];
System.arraycopy(transactionSignature, 8, expectedPartialSignature, 0, expectedPartialSignature.length);
byte[] actualPartialSignature = new byte[24];
System.arraycopy(rawNextTimestamp, 8, actualPartialSignature, 0, actualPartialSignature.length);
assertArrayEquals(expectedPartialSignature, actualPartialSignature);
}
}

View File

@@ -0,0 +1,311 @@
package org.qortal.test.at;
import static org.junit.Assert.*;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
import org.ciyam.at.CompilationException;
import org.ciyam.at.FunctionCode;
import org.ciyam.at.MachineState;
import org.ciyam.at.OpCode;
import org.ciyam.at.Timestamp;
import org.junit.After;
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.at.QortalFunctionCode;
import org.qortal.block.Block;
import org.qortal.data.at.ATStateData;
import org.qortal.data.block.BlockData;
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.transaction.Transaction;
import org.qortal.utils.BitTwiddling;
public class SleepUntilMessageTests extends Common {
private static final byte[] messageData = new byte[] { 0x44 };
private static final byte[] creationBytes = buildSleepUntilMessageAT();
private static final long fundingAmount = 1_00000000L;
private Repository repository = null;
private PrivateKeyAccount deployer;
private DeployAtTransaction deployAtTransaction;
private Account atAccount;
private String atAddress;
private byte[] rawNextTimestamp = new byte[32];
private Transaction transaction;
@Before
public void before() throws DataException {
Common.useDefaultSettings();
this.repository = RepositoryManager.getRepository();
this.deployer = Common.getTestAccount(repository, "alice");
this.deployAtTransaction = doDeploy(repository, deployer, creationBytes, fundingAmount);
this.atAccount = deployAtTransaction.getATAccount();
this.atAddress = deployAtTransaction.getATAccount().getAddress();
}
@After
public void after() throws DataException {
if (this.repository != null)
this.repository.close();
this.repository = null;
}
@Test
public void testDeploy() throws DataException {
// Confirm initial value is zero
extractNextTxTimestamp(repository, atAddress, rawNextTimestamp);
assertArrayEquals(new byte[32], rawNextTimestamp);
}
@Test
public void testFeelessSleep() throws DataException {
// Mint block to allow AT to initialize and call SLEEP_UNTIL_MESSAGE
BlockUtils.mintBlock(repository);
// Fetch AT's balance for this height
long preMintBalance = atAccount.getConfirmedBalance(Asset.QORT);
// Mint block
BlockUtils.mintBlock(repository);
// Fetch new AT balance
long postMintBalance = atAccount.getConfirmedBalance(Asset.QORT);
assertEquals(preMintBalance, postMintBalance);
}
@Test
public void testFeelessSleep2() throws DataException {
// Mint block to allow AT to initialize and call SLEEP_UNTIL_MESSAGE
BlockUtils.mintBlock(repository);
// Fetch AT's balance for this height
long preMintBalance = atAccount.getConfirmedBalance(Asset.QORT);
// Mint several blocks
for (int i = 0; i < 10; ++i)
BlockUtils.mintBlock(repository);
// Fetch new AT balance
long postMintBalance = atAccount.getConfirmedBalance(Asset.QORT);
assertEquals(preMintBalance, postMintBalance);
}
@Test
public void testSleepUntilMessage() throws DataException {
// Mint block to allow AT to initialize and call SLEEP_UNTIL_MESSAGE
BlockUtils.mintBlock(repository);
// Send message to AT
transaction = sendMessage(repository, deployer, messageData, atAddress);
BlockUtils.mintBlock(repository);
// Mint block so AT executes and finds message
BlockUtils.mintBlock(repository);
// Confirm AT finds message
assertTimestamp(repository, atAddress, transaction);
}
private static byte[] buildSleepUntilMessageAT() {
// Labels for data segment addresses
int addrCounter = 0;
// Beginning of data segment for easy extraction
final int addrNextTx = addrCounter;
addrCounter += 4;
final int addrNextTxIndex = addrCounter++;
final int addrLastTxTimestamp = addrCounter++;
// Data segment
ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
// skip addrNextTx
dataByteBuffer.position(dataByteBuffer.position() + 4 * MachineState.VALUE_SIZE);
// Store pointer to addrNextTx at addrNextTxIndex
dataByteBuffer.putLong(addrNextTx);
ByteBuffer codeByteBuffer = ByteBuffer.allocate(512);
// 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, addrLastTxTimestamp));
// Set restart position to after this opcode
codeByteBuffer.put(OpCode.SET_PCS.compile());
/* Loop, waiting for message to AT */
/* Sleep until message arrives */
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.SLEEP_UNTIL_MESSAGE.value, addrLastTxTimestamp));
// 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, addrLastTxTimestamp));
// Copy A to data segment, starting at addrNextTx (as pointed to by addrNextTxIndex)
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_A_IND, addrNextTxIndex));
// Stop if timestamp part of A is zero
codeByteBuffer.put(OpCode.STZ_DAT.compile(addrNextTx));
// 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, addrLastTxTimestamp));
// We're done
codeByteBuffer.put(OpCode.FIN_IMD.compile());
} catch (CompilationException e) {
throw new IllegalStateException("Unable to compile AT?", e);
}
}
codeByteBuffer.flip();
byte[] codeBytes = new byte[codeByteBuffer.limit()];
codeByteBuffer.get(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);
}
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, byte[] creationBytes, long fundingAmount) throws DataException {
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 = "Test AT";
String description = "Test AT";
String atType = "Test";
String tags = "TEST";
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 void extractNextTxTimestamp(Repository repository, String atAddress, byte[] rawNextTimestamp) throws DataException {
// Check AT result
ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress);
byte[] stateData = atStateData.getStateData();
byte[] dataBytes = MachineState.extractDataBytes(stateData);
System.arraycopy(dataBytes, 0, rawNextTimestamp, 0, rawNextTimestamp.length);
}
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.signAndImportValid(repository, messageTransactionData, sender);
return messageTransaction;
}
private void assertTimestamp(Repository repository, String atAddress, Transaction transaction) throws DataException {
int height = transaction.getHeight();
byte[] transactionSignature = transaction.getTransactionData().getSignature();
BlockData blockData = repository.getBlockRepository().fromHeight(height);
assertNotNull(blockData);
Block block = new Block(repository, blockData);
List<Transaction> blockTransactions = block.getTransactions();
int sequence;
for (sequence = blockTransactions.size() - 1; sequence >= 0; --sequence)
if (Arrays.equals(blockTransactions.get(sequence).getTransactionData().getSignature(), transactionSignature))
break;
assertNotSame(-1, sequence);
byte[] rawNextTimestamp = new byte[32];
extractNextTxTimestamp(repository, atAddress, rawNextTimestamp);
Timestamp expectedTimestamp = new Timestamp(height, sequence);
Timestamp actualTimestamp = new Timestamp(BitTwiddling.longFromBEBytes(rawNextTimestamp, 0));
assertEquals(String.format("Expected height %d, seq %d but was height %d, seq %d",
height, sequence,
actualTimestamp.blockHeight, actualTimestamp.transactionSequence
),
expectedTimestamp.longValue(),
actualTimestamp.longValue());
byte[] expectedPartialSignature = new byte[24];
System.arraycopy(transactionSignature, 8, expectedPartialSignature, 0, expectedPartialSignature.length);
byte[] actualPartialSignature = new byte[24];
System.arraycopy(rawNextTimestamp, 8, actualPartialSignature, 0, actualPartialSignature.length);
assertArrayEquals(expectedPartialSignature, actualPartialSignature);
}
}

View File

@@ -35,10 +35,10 @@ public class DogecoinTests extends Common {
@Test
public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException {
long before = System.currentTimeMillis();
System.out.println(String.format("Bitcoin median blocktime: %d", dogecoin.getMedianBlockTime()));
System.out.println(String.format("Dogecoin median blocktime: %d", dogecoin.getMedianBlockTime()));
long afterFirst = System.currentTimeMillis();
System.out.println(String.format("Bitcoin median blocktime: %d", dogecoin.getMedianBlockTime()));
System.out.println(String.format("Dogecoin median blocktime: %d", dogecoin.getMedianBlockTime()));
long afterSecond = System.currentTimeMillis();
long firstPeriod = afterFirst - before;
@@ -64,10 +64,11 @@ public class DogecoinTests extends Common {
}
@Test
@Ignore(value = "No testnet nodes available, so we can't regularly test buildSpend yet")
public void testBuildSpend() {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
String xprv58 = "dgpv51eADS3spNJh9drNeW1Tc1P9z2LyaQRXPBortsq6yice1k47C2u2Prvgxycr2ihNBWzKZ2LthcBBGiYkWZ69KUTVkcLVbnjq7pD8mnApEru";
String recipient = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
String recipient = "DP1iFao33xdEPa5vaArpj7sykfzKNeiJeX";
long amount = 1000L;
Transaction transaction = dogecoin.buildSpend(xprv58, recipient, amount);
@@ -81,7 +82,7 @@ public class DogecoinTests extends Common {
@Test
public void testGetWalletBalance() {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
String xprv58 = "dgpv51eADS3spNJh9drNeW1Tc1P9z2LyaQRXPBortsq6yice1k47C2u2Prvgxycr2ihNBWzKZ2LthcBBGiYkWZ69KUTVkcLVbnjq7pD8mnApEru";
Long balance = dogecoin.getWalletBalance(xprv58);
@@ -102,7 +103,7 @@ public class DogecoinTests extends Common {
@Test
public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
String xprv58 = "dgpv51eADS3spNJh9drNeW1Tc1P9z2LyaQRXPBortsq6yice1k47C2u2Prvgxycr2ihNBWzKZ2LthcBBGiYkWZ69KUTVkcLVbnjq7pD8mnApEru";
String address = dogecoin.getUnusedReceiveAddress(xprv58);