diff --git a/lib/org/ciyam/AT/1.3.4/AT-1.3.4.jar b/lib/org/ciyam/AT/1.3.4/AT-1.3.4.jar new file mode 100644 index 00000000..5abe2c77 Binary files /dev/null and b/lib/org/ciyam/AT/1.3.4/AT-1.3.4.jar differ diff --git a/lib/org/ciyam/at/1.0/at-1.0.pom b/lib/org/ciyam/AT/1.3.4/AT-1.3.4.pom similarity index 87% rename from lib/org/ciyam/at/1.0/at-1.0.pom rename to lib/org/ciyam/AT/1.3.4/AT-1.3.4.pom index 973d4371..39af6ac7 100644 --- a/lib/org/ciyam/at/1.0/at-1.0.pom +++ b/lib/org/ciyam/AT/1.3.4/AT-1.3.4.pom @@ -3,7 +3,7 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> 4.0.0 org.ciyam - at - 1.0 + AT + 1.3.4 POM was created from install:install-file diff --git a/lib/org/ciyam/at/maven-metadata-local.xml b/lib/org/ciyam/AT/maven-metadata-local.xml similarity index 53% rename from lib/org/ciyam/at/maven-metadata-local.xml rename to lib/org/ciyam/AT/maven-metadata-local.xml index 61f9d592..2cf6d13a 100644 --- a/lib/org/ciyam/at/maven-metadata-local.xml +++ b/lib/org/ciyam/AT/maven-metadata-local.xml @@ -1,12 +1,12 @@ org.ciyam - at + AT - 1.0 + 1.3.4 - 1.0 + 1.3.4 - 20181105100741 + 20200414162728 diff --git a/lib/org/ciyam/at/1.0/at-1.0.jar b/lib/org/ciyam/at/1.0/at-1.0.jar deleted file mode 100644 index a5dd4957..00000000 Binary files a/lib/org/ciyam/at/1.0/at-1.0.jar and /dev/null differ diff --git a/pom.xml b/pom.xml index c1819294..27dc28a7 100644 --- a/pom.xml +++ b/pom.xml @@ -6,9 +6,10 @@ 1.0.7 jar - 0.15.4 + 0.15.5 1.64 ${maven.build.timestamp} + 1.3.4 3.6 1.8 1.2.2 @@ -405,14 +406,14 @@ org.ciyam - at - 1.0 + AT + ${ciyam-at.version} org.bitcoinj bitcoinj-core - ${bitcoin.version} + ${bitcoinj.version} diff --git a/src/main/java/org/qortal/api/ApiError.java b/src/main/java/org/qortal/api/ApiError.java index 8a0b6078..14ab1f4f 100644 --- a/src/main/java/org/qortal/api/ApiError.java +++ b/src/main/java/org/qortal/api/ApiError.java @@ -118,7 +118,12 @@ public enum ApiError { // MESSAGESIZE_EXCEEDED(1004, 400), // Groups - GROUP_UNKNOWN(1101, 404); + GROUP_UNKNOWN(1101, 404), + + // Bitcoin + BTC_NETWORK_ISSUE(1201, 500), + BTC_BALANCE_ISSUE(1202, 422), + BTC_TOO_SOON(1203, 422); private static final Map map = stream(ApiError.values()).collect(toMap(apiError -> apiError.code, apiError -> apiError)); diff --git a/src/main/java/org/qortal/api/model/CrossChainBitcoinP2SHStatus.java b/src/main/java/org/qortal/api/model/CrossChainBitcoinP2SHStatus.java new file mode 100644 index 00000000..ff986e86 --- /dev/null +++ b/src/main/java/org/qortal/api/model/CrossChainBitcoinP2SHStatus.java @@ -0,0 +1,31 @@ +package org.qortal.api.model; + +import java.math.BigDecimal; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; + +import io.swagger.v3.oas.annotations.media.Schema; + +@XmlAccessorType(XmlAccessType.FIELD) +public class CrossChainBitcoinP2SHStatus { + + @Schema(description = "Bitcoin P2SH address", example = "3CdH27kTpV8dcFHVRYjQ8EEV5FJg9X8pSJ (mainnet), 2fMiRRXVsxhZeyfum9ifybZvaMHbQTmwdZw (testnet)") + public String bitcoinP2shAddress; + + @Schema(description = "Bitcoin P2SH balance") + public BigDecimal bitcoinP2shBalance; + + @Schema(description = "Can P2SH redeem yet?") + public boolean canRedeem; + + @Schema(description = "Can P2SH refund yet?") + public boolean canRefund; + + @Schema(description = "Secret extracted by P2SH redeemer") + public byte[] secret; + + public CrossChainBitcoinP2SHStatus() { + } + +} diff --git a/src/main/java/org/qortal/api/model/CrossChainBitcoinRedeemRequest.java b/src/main/java/org/qortal/api/model/CrossChainBitcoinRedeemRequest.java new file mode 100644 index 00000000..fcbf2ec4 --- /dev/null +++ b/src/main/java/org/qortal/api/model/CrossChainBitcoinRedeemRequest.java @@ -0,0 +1,31 @@ +package org.qortal.api.model; + +import java.math.BigDecimal; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; + +import io.swagger.v3.oas.annotations.media.Schema; + +@XmlAccessorType(XmlAccessType.FIELD) +public class CrossChainBitcoinRedeemRequest { + + @Schema(description = "Bitcoin P2PKH address for refund", example = "1BwG6aG2GapFX5b4JT4ohbsYvj1xZ8d2EJ (mainnet), mrTDPdM15cFWJC4g223BXX5snicfVJBx6M (testnet)") + public String refundAddress; + + @Schema(description = "Bitcoin PRIVATE KEY for redeem", example = "cSP3zTb6bfm8GATtAcEJ8LqYtNQmzZ9jE2wQUVnZGiBzojDdrwKV") + public byte[] redeemPrivateKey; + + @Schema(description = "Qortal AT address") + public String atAddress; + + @Schema(description = "Bitcoin miner fee", example = "0.00001000") + public BigDecimal bitcoinMinerFee; + + @Schema(description = "32-byte secret", example = "6gVbAXCVzJXAWwtAVGAfgAkkXpeXvPUwSciPmCfSfXJG") + public byte[] secret; + + public CrossChainBitcoinRedeemRequest() { + } + +} diff --git a/src/main/java/org/qortal/api/model/CrossChainBitcoinRefundRequest.java b/src/main/java/org/qortal/api/model/CrossChainBitcoinRefundRequest.java new file mode 100644 index 00000000..490ee935 --- /dev/null +++ b/src/main/java/org/qortal/api/model/CrossChainBitcoinRefundRequest.java @@ -0,0 +1,28 @@ +package org.qortal.api.model; + +import java.math.BigDecimal; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; + +import io.swagger.v3.oas.annotations.media.Schema; + +@XmlAccessorType(XmlAccessType.FIELD) +public class CrossChainBitcoinRefundRequest { + + @Schema(description = "Bitcoin PRIVATE KEY for refund", example = "cSP3zTb6bfm8GATtAcEJ8LqYtNQmzZ9jE2wQUVnZGiBzojDdrwKV") + public byte[] refundPrivateKey; + + @Schema(description = "Bitcoin P2PKH address for redeem", example = "1BwG6aG2GapFX5b4JT4ohbsYvj1xZ8d2EJ (mainnet), mrTDPdM15cFWJC4g223BXX5snicfVJBx6M (testnet)") + public String redeemAddress; + + @Schema(description = "Qortal AT address") + public String atAddress; + + @Schema(description = "Bitcoin miner fee", example = "0.00001000") + public BigDecimal bitcoinMinerFee; + + public CrossChainBitcoinRefundRequest() { + } + +} diff --git a/src/main/java/org/qortal/api/model/CrossChainBitcoinTemplateRequest.java b/src/main/java/org/qortal/api/model/CrossChainBitcoinTemplateRequest.java new file mode 100644 index 00000000..c23815bb --- /dev/null +++ b/src/main/java/org/qortal/api/model/CrossChainBitcoinTemplateRequest.java @@ -0,0 +1,23 @@ +package org.qortal.api.model; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; + +import io.swagger.v3.oas.annotations.media.Schema; + +@XmlAccessorType(XmlAccessType.FIELD) +public class CrossChainBitcoinTemplateRequest { + + @Schema(description = "Bitcoin P2PKH address for refund", example = "1BwG6aG2GapFX5b4JT4ohbsYvj1xZ8d2EJ (mainnet), mrTDPdM15cFWJC4g223BXX5snicfVJBx6M (testnet)") + public String refundAddress; + + @Schema(description = "Bitcoin P2PKH address for redeem", example = "1BwG6aG2GapFX5b4JT4ohbsYvj1xZ8d2EJ (mainnet), mrTDPdM15cFWJC4g223BXX5snicfVJBx6M (testnet)") + public String redeemAddress; + + @Schema(description = "Qortal AT address") + public String atAddress; + + public CrossChainBitcoinTemplateRequest() { + } + +} diff --git a/src/main/java/org/qortal/api/model/CrossChainBuildRequest.java b/src/main/java/org/qortal/api/model/CrossChainBuildRequest.java new file mode 100644 index 00000000..c4fa097a --- /dev/null +++ b/src/main/java/org/qortal/api/model/CrossChainBuildRequest.java @@ -0,0 +1,40 @@ +package org.qortal.api.model; + +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 CrossChainBuildRequest { + + @Schema(description = "AT creator's public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry") + public byte[] creatorPublicKey; + + @Schema(description = "Initial QORT amount paid when trade agreed", example = "0.00100000") + @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) + public long initialQortAmount; + + @Schema(description = "Final QORT amount paid out on successful trade", example = "80.40200000") + @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) + public long finalQortAmount; + + @Schema(description = "QORT amount funding AT, including covering AT execution fees", example = "123.45670000") + @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) + public long fundingQortAmount; + + @Schema(description = "HASH160 of secret", example = "43vnftqkjxrhb5kJdkU1ZFQLEnWV") + public byte[] secretHash; + + @Schema(description = "Bitcoin P2SH BTC balance for release of secret", example = "0.00864200") + @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) + public long bitcoinAmount; + + @Schema(description = "Trade time window (minutes) from trade agreement to automatic refund", example = "10080") + public Integer tradeTimeout; + + public CrossChainBuildRequest() { + } + +} diff --git a/src/main/java/org/qortal/api/model/CrossChainCancelRequest.java b/src/main/java/org/qortal/api/model/CrossChainCancelRequest.java new file mode 100644 index 00000000..e1f57a7e --- /dev/null +++ b/src/main/java/org/qortal/api/model/CrossChainCancelRequest.java @@ -0,0 +1,20 @@ +package org.qortal.api.model; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; + +import io.swagger.v3.oas.annotations.media.Schema; + +@XmlAccessorType(XmlAccessType.FIELD) +public class CrossChainCancelRequest { + + @Schema(description = "AT creator's public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry") + public byte[] creatorPublicKey; + + @Schema(description = "Qortal AT address") + public String atAddress; + + public CrossChainCancelRequest() { + } + +} diff --git a/src/main/java/org/qortal/api/model/CrossChainSecretRequest.java b/src/main/java/org/qortal/api/model/CrossChainSecretRequest.java new file mode 100644 index 00000000..99820022 --- /dev/null +++ b/src/main/java/org/qortal/api/model/CrossChainSecretRequest.java @@ -0,0 +1,23 @@ +package org.qortal.api.model; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; + +import io.swagger.v3.oas.annotations.media.Schema; + +@XmlAccessorType(XmlAccessType.FIELD) +public class CrossChainSecretRequest { + + @Schema(description = "Public key to match AT's 'recipient'", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry") + public byte[] recipientPublicKey; + + @Schema(description = "Qortal AT address") + public String atAddress; + + @Schema(description = "32-byte secret", example = "6gVbAXCVzJXAWwtAVGAfgAkkXpeXvPUwSciPmCfSfXJG") + public byte[] secret; + + public CrossChainSecretRequest() { + } + +} diff --git a/src/main/java/org/qortal/api/model/CrossChainTradeRequest.java b/src/main/java/org/qortal/api/model/CrossChainTradeRequest.java new file mode 100644 index 00000000..32737dd5 --- /dev/null +++ b/src/main/java/org/qortal/api/model/CrossChainTradeRequest.java @@ -0,0 +1,23 @@ +package org.qortal.api.model; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; + +import io.swagger.v3.oas.annotations.media.Schema; + +@XmlAccessorType(XmlAccessType.FIELD) +public class CrossChainTradeRequest { + + @Schema(description = "AT creator's public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry") + public byte[] creatorPublicKey; + + @Schema(description = "Qortal AT address") + public String atAddress; + + @Schema(description = "Qortal address for trade partner/recipient") + public String recipient; + + public CrossChainTradeRequest() { + } + +} diff --git a/src/main/java/org/qortal/api/resource/ApiDefinition.java b/src/main/java/org/qortal/api/resource/ApiDefinition.java index 52b6ade0..c887d4bc 100644 --- a/src/main/java/org/qortal/api/resource/ApiDefinition.java +++ b/src/main/java/org/qortal/api/resource/ApiDefinition.java @@ -13,7 +13,9 @@ import io.swagger.v3.oas.annotations.tags.Tag; @Tag(name = "Admin"), @Tag(name = "Arbitrary"), @Tag(name = "Assets"), + @Tag(name = "Automated Transactions"), @Tag(name = "Blocks"), + @Tag(name = "Cross-Chain"), @Tag(name = "Groups"), @Tag(name = "Names"), @Tag(name = "Payments"), diff --git a/src/main/java/org/qortal/api/resource/AtResource.java b/src/main/java/org/qortal/api/resource/AtResource.java new file mode 100644 index 00000000..86b79da9 --- /dev/null +++ b/src/main/java/org/qortal/api/resource/AtResource.java @@ -0,0 +1,206 @@ +package org.qortal.api.resource; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +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.tags.Tag; + +import java.util.List; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; + +import org.ciyam.at.MachineState; +import org.qortal.api.ApiError; +import org.qortal.api.ApiErrors; +import org.qortal.api.ApiException; +import org.qortal.api.ApiExceptionFactory; +import org.qortal.at.QortalAtLoggerFactory; +import org.qortal.data.at.ATData; +import org.qortal.data.at.ATStateData; +import org.qortal.data.transaction.DeployAtTransactionData; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; +import org.qortal.settings.Settings; +import org.qortal.transaction.Transaction; +import org.qortal.transaction.Transaction.ValidationResult; +import org.qortal.transform.TransformationException; +import org.qortal.transform.transaction.DeployAtTransactionTransformer; +import org.qortal.utils.Base58; + +@Path("/at") +@Tag(name = "Automated Transactions") +public class AtResource { + + @Context + HttpServletRequest request; + + @GET + @Path("/byfunction/{codehash}") + @Operation( + summary = "Find automated transactions with matching functionality (code hash)", + responses = { + @ApiResponse( + description = "automated transactions", + content = @Content( + array = @ArraySchema( + schema = @Schema( + implementation = ATData.class + ) + ) + ) + ) + } + ) + @ApiErrors({ + ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE + }) + public List getByFunctionality( + @PathParam("codehash") + String codeHash58, + @Parameter(description = "whether to include ATs that can run, or not, or both (if omitted)") + @QueryParam("isExecutable") + Boolean isExecutable, + @Parameter( ref = "limit") @QueryParam("limit") Integer limit, + @Parameter( ref = "offset" ) @QueryParam("offset") Integer offset, + @Parameter( ref = "reverse" ) @QueryParam("reverse") Boolean reverse) { + // Decode codeHash + byte[] codeHash; + try { + codeHash = Base58.decode(codeHash58); + } catch (NumberFormatException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA, e); + } + + // codeHash must be present and have correct length + if (codeHash == null || codeHash.length != 32) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + + // Impose a limit on 'limit' + if (limit != null && limit > 100) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + + try (final Repository repository = RepositoryManager.getRepository()) { + return repository.getATRepository().getATsByFunctionality(codeHash, isExecutable, limit, offset, reverse); + } catch (ApiException e) { + throw e; + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + + @GET + @Path("/{ataddress}") + @Operation( + summary = "Fetch info associated with AT address", + responses = { + @ApiResponse( + description = "automated transaction", + content = @Content( + schema = @Schema(implementation = ATData.class) + ) + ) + } + ) + @ApiErrors({ + ApiError.REPOSITORY_ISSUE + }) + public ATData getByAddress(@PathParam("ataddress") String atAddress) { + try (final Repository repository = RepositoryManager.getRepository()) { + return repository.getATRepository().fromATAddress(atAddress); + } catch (ApiException e) { + throw e; + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + + @GET + @Path("/{ataddress}/data") + @Operation( + summary = "Fetch data segment associated with AT address", + responses = { + @ApiResponse( + description = "automated transaction", + content = @Content( + schema = @Schema(implementation = byte[].class) + ) + ) + } + ) + @ApiErrors({ + ApiError.REPOSITORY_ISSUE + }) + public byte[] getDataByAddress(@PathParam("ataddress") String atAddress) { + try (final Repository repository = RepositoryManager.getRepository()) { + ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress); + byte[] stateData = atStateData.getStateData(); + + QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance(); + byte[] dataBytes = MachineState.extractDataBytes(loggerFactory, stateData); + + return dataBytes; + } catch (ApiException e) { + throw e; + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + + @POST + @Operation( + summary = "Build raw, unsigned, DEPLOY_AT transaction", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = DeployAtTransactionData.class + ) + ) + ), + responses = { + @ApiResponse( + description = "raw, unsigned, DEPLOY_AT transaction encoded in Base58", + content = @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema( + type = "string" + ) + ) + ) + } + ) + @ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) + public String createDeployAt(DeployAtTransactionData transactionData) { + if (Settings.getInstance().isApiRestricted()) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION); + + try (final Repository repository = RepositoryManager.getRepository()) { + Transaction transaction = Transaction.fromData(repository, transactionData); + + ValidationResult result = transaction.isValidUnconfirmed(); + if (result != ValidationResult.OK) + throw TransactionsResource.createTransactionInvalidException(request, result); + + byte[] bytes = DeployAtTransactionTransformer.toBytes(transactionData); + return Base58.encode(bytes); + } catch (TransformationException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e); + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + +} \ No newline at end of file diff --git a/src/main/java/org/qortal/api/resource/CrossChainResource.java b/src/main/java/org/qortal/api/resource/CrossChainResource.java new file mode 100644 index 00000000..44c1ee6a --- /dev/null +++ b/src/main/java/org/qortal/api/resource/CrossChainResource.java @@ -0,0 +1,819 @@ +package org.qortal.api.resource; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +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.tags.Tag; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.LegacyAddress; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.TransactionOutput; +import org.bitcoinj.script.Script.ScriptType; +import org.bitcoinj.wallet.WalletTransaction; +import org.qortal.account.PublicKeyAccount; +import org.qortal.api.ApiError; +import org.qortal.api.ApiErrors; +import org.qortal.api.ApiException; +import org.qortal.api.ApiExceptionFactory; +import org.qortal.api.model.CrossChainCancelRequest; +import org.qortal.api.model.CrossChainSecretRequest; +import org.qortal.api.model.CrossChainTradeRequest; +import org.qortal.api.model.CrossChainBitcoinP2SHStatus; +import org.qortal.api.model.CrossChainBitcoinRedeemRequest; +import org.qortal.api.model.CrossChainBitcoinRefundRequest; +import org.qortal.api.model.CrossChainBitcoinTemplateRequest; +import org.qortal.api.model.CrossChainBuildRequest; +import org.qortal.asset.Asset; +import org.qortal.crosschain.BTC; +import org.qortal.crosschain.BTCACCT; +import org.qortal.crypto.Crypto; +import org.qortal.data.at.ATData; +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.transaction.DeployAtTransaction; +import org.qortal.transaction.MessageTransaction; +import org.qortal.transaction.Transaction; +import org.qortal.transaction.Transaction.ValidationResult; +import org.qortal.transform.TransformationException; +import org.qortal.transform.Transformer; +import org.qortal.transform.transaction.DeployAtTransactionTransformer; +import org.qortal.transform.transaction.MessageTransactionTransformer; +import org.qortal.utils.Base58; +import org.qortal.utils.NTP; + +import com.google.common.primitives.Bytes; + +@Path("/crosschain") +@Tag(name = "Cross-Chain") +public class CrossChainResource { + + @Context + HttpServletRequest request; + + @GET + @Path("/tradeoffers") + @Operation( + summary = "Find cross-chain trade offers", + responses = { + @ApiResponse( + description = "automated transactions", + content = @Content( + array = @ArraySchema( + schema = @Schema( + implementation = CrossChainTradeData.class + ) + ) + ) + ) + } + ) + @ApiErrors({ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE}) + public List getTradeOffers( + @Parameter( ref = "limit") @QueryParam("limit") Integer limit, + @Parameter( ref = "offset" ) @QueryParam("offset") Integer offset, + @Parameter( ref = "reverse" ) @QueryParam("reverse") Boolean reverse) { + // Impose a limit on 'limit' + if (limit != null && limit > 100) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + + byte[] codeHash = BTCACCT.CODE_BYTES_HASH; + boolean isExecutable = true; + + try (final Repository repository = RepositoryManager.getRepository()) { + List atsData = repository.getATRepository().getATsByFunctionality(codeHash, isExecutable, limit, offset, reverse); + + List crossChainTradesData = new ArrayList<>(); + for (ATData atData : atsData) { + CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); + crossChainTradesData.add(crossChainTradeData); + } + + return crossChainTradesData; + } catch (ApiException e) { + throw e; + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + + @POST + @Path("/build") + @Operation( + summary = "Build cross-chain trading AT", + description = "Returns raw, unsigned DEPLOY_AT transaction", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = CrossChainBuildRequest.class + ) + ) + ), + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string")) + ) + } + ) + @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_DATA, ApiError.INVALID_REFERENCE, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE}) + public String buildTrade(CrossChainBuildRequest tradeRequest) { + byte[] creatorPublicKey = tradeRequest.creatorPublicKey; + + if (creatorPublicKey == null || creatorPublicKey.length != Transformer.PUBLIC_KEY_LENGTH) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY); + + if (tradeRequest.secretHash == null || tradeRequest.secretHash.length != BTC.HASH160_LENGTH) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); + + if (tradeRequest.tradeTimeout == null) + tradeRequest.tradeTimeout = 7 * 24 * 60; // 7 days + else + if (tradeRequest.tradeTimeout < 10 || tradeRequest.tradeTimeout > 50000) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); + + if (tradeRequest.initialQortAmount < 0) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); + + if (tradeRequest.finalQortAmount <= 0) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); + + if (tradeRequest.fundingQortAmount <= 0) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); + + // funding amount must exceed initial + final + if (tradeRequest.fundingQortAmount <= tradeRequest.initialQortAmount + tradeRequest.finalQortAmount) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); + + if (tradeRequest.bitcoinAmount <= 0) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); + + try (final Repository repository = RepositoryManager.getRepository()) { + PublicKeyAccount creatorAccount = new PublicKeyAccount(repository, creatorPublicKey); + + byte[] creationBytes = BTCACCT.buildQortalAT(creatorAccount.getAddress(), tradeRequest.secretHash, tradeRequest.tradeTimeout, tradeRequest.initialQortAmount, tradeRequest.finalQortAmount, tradeRequest.bitcoinAmount); + + long txTimestamp = NTP.getTime(); + byte[] lastReference = creatorAccount.getLastReference(); + if (lastReference == null) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_REFERENCE); + + long fee = 0; + String name = "QORT-BTC cross-chain trade"; + String description = "Qortal-Bitcoin cross-chain trade"; + String atType = "ACCT"; + String tags = "QORT-BTC ACCT"; + + BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, creatorAccount.getPublicKey(), fee, null); + TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, tradeRequest.fundingQortAmount, Asset.QORT); + + Transaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData); + + fee = deployAtTransaction.calcRecommendedFee(); + deployAtTransactionData.setFee(fee); + + ValidationResult result = deployAtTransaction.isValidUnconfirmed(); + if (result != ValidationResult.OK) + throw TransactionsResource.createTransactionInvalidException(request, result); + + byte[] bytes = DeployAtTransactionTransformer.toBytes(deployAtTransactionData); + return Base58.encode(bytes); + } catch (TransformationException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e); + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + + @POST + @Path("/tradeoffer/recipient") + @Operation( + summary = "Builds raw, unsigned MESSAGE transaction that sends cross-chain trade recipient address, triggering 'trade' mode", + description = "Specify address of cross-chain AT that needs to be messaged, and address of Qortal recipient.
" + + "AT needs to be in 'offer' mode. Messages sent to an AT in 'trade' mode will be ignored, but still cost fees to send!
" + + "You need to sign output with same account as the AT creator otherwise the MESSAGE transaction will be invalid.", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = CrossChainTradeRequest.class + ) + ) + ), + responses = { + @ApiResponse( + content = @Content( + schema = @Schema( + type = "string" + ) + ) + ) + } + ) + @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE}) + public String sendTradeRecipient(CrossChainTradeRequest tradeRequest) { + byte[] creatorPublicKey = tradeRequest.creatorPublicKey; + + if (creatorPublicKey == null || creatorPublicKey.length != Transformer.PUBLIC_KEY_LENGTH) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY); + + if (tradeRequest.atAddress == null || !Crypto.isValidAtAddress(tradeRequest.atAddress)) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + + if (tradeRequest.recipient == null || !Crypto.isValidAddress(tradeRequest.recipient)) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + + try (final Repository repository = RepositoryManager.getRepository()) { + ATData atData = fetchAtDataWithChecking(repository, creatorPublicKey, tradeRequest.atAddress); + CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); + + if (crossChainTradeData.mode == CrossChainTradeData.Mode.TRADE) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + + // Good to make MESSAGE + + byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(tradeRequest.recipient), 32, 0); + byte[] messageTransactionBytes = buildAtMessage(repository, creatorPublicKey, tradeRequest.atAddress, recipientAddressBytes); + + return Base58.encode(messageTransactionBytes); + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + + @POST + @Path("/tradeoffer/secret") + @Operation( + summary = "Builds raw, unsigned MESSAGE transaction that sends secret to AT, releasing funds to recipient", + description = "Specify address of cross-chain AT that needs to be messaged, and 32-byte secret.
" + + "AT needs to be in 'trade' mode. Messages sent to an AT in 'trade' mode will be ignored, but still cost fees to send!
" + + "You need to sign output with account the AT considers the 'recipient' otherwise the MESSAGE transaction will be invalid.", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = CrossChainSecretRequest.class + ) + ) + ), + responses = { + @ApiResponse( + content = @Content( + schema = @Schema( + type = "string" + ) + ) + ) + } + ) + @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_DATA, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE}) + public String sendSecret(CrossChainSecretRequest secretRequest) { + byte[] recipientPublicKey = secretRequest.recipientPublicKey; + + if (recipientPublicKey == null || recipientPublicKey.length != Transformer.PUBLIC_KEY_LENGTH) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY); + + if (secretRequest.atAddress == null || !Crypto.isValidAtAddress(secretRequest.atAddress)) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + + if (secretRequest.secret == null || secretRequest.secret.length != BTCACCT.SECRET_LENGTH) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); + + try (final Repository repository = RepositoryManager.getRepository()) { + ATData atData = fetchAtDataWithChecking(repository, null, secretRequest.atAddress); // null to skip creator check + CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); + + if (crossChainTradeData.mode == CrossChainTradeData.Mode.OFFER) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + + PublicKeyAccount recipientAccount = new PublicKeyAccount(repository, recipientPublicKey); + String recipientAddress = recipientAccount.getAddress(); + + // MESSAGE must come from address that AT considers trade partner / 'recipient' + if (!crossChainTradeData.qortalRecipient.equals(recipientAddress)) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + + // Good to make MESSAGE + + byte[] messageTransactionBytes = buildAtMessage(repository, recipientPublicKey, secretRequest.atAddress, secretRequest.secret); + + return Base58.encode(messageTransactionBytes); + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + + @DELETE + @Path("/tradeoffer") + @Operation( + summary = "Builds raw, unsigned MESSAGE transaction that cancels cross-chain trade offer", + description = "Specify address of cross-chain AT that needs to be cancelled.
" + + "AT needs to be in 'offer' mode. Messages sent to an AT in 'trade' mode will be ignored, but still cost fees to send!
" + + "You need to sign output with same account as the AT creator otherwise the MESSAGE transaction will be invalid.", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = CrossChainCancelRequest.class + ) + ) + ), + responses = { + @ApiResponse( + content = @Content( + schema = @Schema( + type = "string" + ) + ) + ) + } + ) + @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE}) + public String cancelTradeOffer(CrossChainCancelRequest cancelRequest) { + byte[] creatorPublicKey = cancelRequest.creatorPublicKey; + + if (creatorPublicKey == null || creatorPublicKey.length != Transformer.PUBLIC_KEY_LENGTH) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY); + + if (cancelRequest.atAddress == null || !Crypto.isValidAtAddress(cancelRequest.atAddress)) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + + try (final Repository repository = RepositoryManager.getRepository()) { + ATData atData = fetchAtDataWithChecking(repository, creatorPublicKey, cancelRequest.atAddress); + CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); + + if (crossChainTradeData.mode == CrossChainTradeData.Mode.TRADE) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + + // Good to make MESSAGE + + PublicKeyAccount creatorAccount = new PublicKeyAccount(repository, creatorPublicKey); + String creatorAddress = creatorAccount.getAddress(); + byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(creatorAddress), 32, 0); + + byte[] messageTransactionBytes = buildAtMessage(repository, creatorPublicKey, cancelRequest.atAddress, recipientAddressBytes); + + return Base58.encode(messageTransactionBytes); + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + + @POST + @Path("/p2sh") + @Operation( + summary = "Returns Bitcoin P2SH address based on trade info", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = CrossChainBitcoinTemplateRequest.class + ) + ) + ), + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string")) + ) + } + ) + @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE}) + public String deriveP2sh(CrossChainBitcoinTemplateRequest templateRequest) { + BTC btc = BTC.getInstance(); + NetworkParameters params = btc.getNetworkParameters(); + + Address refundBitcoinAddress = null; + Address redeemBitcoinAddress = null; + + try { + if (templateRequest.refundAddress == null) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY); + + refundBitcoinAddress = Address.fromString(params, templateRequest.refundAddress); + if (refundBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + } catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + } + + try { + if (templateRequest.redeemAddress == null) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY); + + redeemBitcoinAddress = Address.fromString(params, templateRequest.redeemAddress); + if (redeemBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + } catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + } + + if (templateRequest.atAddress == null || !Crypto.isValidAtAddress(templateRequest.atAddress)) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + + // Extract data from cross-chain trading AT + try (final Repository repository = RepositoryManager.getRepository()) { + ATData atData = fetchAtDataWithChecking(repository, null, templateRequest.atAddress); // null to skip creator check + CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); + + byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), crossChainTradeData.lockTime, redeemBitcoinAddress.getHash(), crossChainTradeData.secretHash); + byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes); + + Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); + return p2shAddress.toString(); + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + + @POST + @Path("/p2sh/check") + @Operation( + summary = "Checks Bitcoin P2SH address based on trade info", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = CrossChainBitcoinTemplateRequest.class + ) + ) + ), + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = CrossChainBitcoinP2SHStatus.class)) + ) + } + ) + @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, ApiError.REPOSITORY_ISSUE}) + public CrossChainBitcoinP2SHStatus checkP2sh(CrossChainBitcoinTemplateRequest templateRequest) { + BTC btc = BTC.getInstance(); + NetworkParameters params = btc.getNetworkParameters(); + + Address refundBitcoinAddress = null; + Address redeemBitcoinAddress = null; + + try { + if (templateRequest.refundAddress == null) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY); + + refundBitcoinAddress = Address.fromString(params, templateRequest.refundAddress); + if (refundBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + } catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + } + + try { + if (templateRequest.redeemAddress == null) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY); + + redeemBitcoinAddress = Address.fromString(params, templateRequest.redeemAddress); + if (redeemBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + } catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + } + + if (templateRequest.atAddress == null || !Crypto.isValidAtAddress(templateRequest.atAddress)) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + + // Extract data from cross-chain trading AT + try (final Repository repository = RepositoryManager.getRepository()) { + ATData atData = fetchAtDataWithChecking(repository, null, templateRequest.atAddress); // null to skip creator check + CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); + + byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), crossChainTradeData.lockTime, redeemBitcoinAddress.getHash(), crossChainTradeData.secretHash); + byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes); + + Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); + + long medianBlockTime = BTC.getInstance().getMedianBlockTime(); + long now = NTP.getTime(); + + // Check P2SH is funded + final int startTime = (int) (crossChainTradeData.tradeModeTimestamp / 1000L); + List fundingOutputs = new ArrayList<>(); + List walletTransactions = new ArrayList<>(); + + Coin p2shBalance = BTC.getInstance().getBalanceAndOtherInfo(p2shAddress.toString(), startTime, fundingOutputs, walletTransactions); + if (p2shBalance == null) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN); + + CrossChainBitcoinP2SHStatus p2shStatus = new CrossChainBitcoinP2SHStatus(); + p2shStatus.bitcoinP2shAddress = p2shAddress.toString(); + p2shStatus.bitcoinP2shBalance = BigDecimal.valueOf(p2shBalance.value, 8); + + if (p2shBalance.value >= crossChainTradeData.expectedBitcoin && fundingOutputs.size() == 1) { + p2shStatus.canRedeem = now >= medianBlockTime * 1000L; + p2shStatus.canRefund = now >= crossChainTradeData.lockTime * 1000L; + } + + if (now >= medianBlockTime * 1000L) { + // See if we can extract secret + p2shStatus.secret = BTCACCT.findP2shSecret(p2shStatus.bitcoinP2shAddress, walletTransactions); + } + + return p2shStatus; + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + + @POST + @Path("/p2sh/refund") + @Operation( + summary = "Returns serialized Bitcoin transaction attempting refund from P2SH address", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = CrossChainBitcoinRefundRequest.class + ) + ) + ), + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string")) + ) + } + ) + @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, + ApiError.BTC_TOO_SOON, ApiError.BTC_BALANCE_ISSUE, ApiError.BTC_NETWORK_ISSUE, ApiError.REPOSITORY_ISSUE}) + public String refundP2sh(CrossChainBitcoinRefundRequest refundRequest) { + BTC btc = BTC.getInstance(); + NetworkParameters params = btc.getNetworkParameters(); + + byte[] refundPrivateKey = refundRequest.refundPrivateKey; + if (refundPrivateKey == null) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY); + + ECKey refundKey = null; + Address redeemBitcoinAddress = null; + + try { + // Auto-trim + if (refundPrivateKey.length >= 37 && refundPrivateKey.length <= 38) + refundPrivateKey = Arrays.copyOfRange(refundPrivateKey, 1, 33); + if (refundPrivateKey.length != 32) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY); + + refundKey = ECKey.fromPrivate(refundPrivateKey); + } catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + } + + try { + if (refundRequest.redeemAddress == null) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY); + + redeemBitcoinAddress = Address.fromString(params, refundRequest.redeemAddress); + if (redeemBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + } catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + } + + if (refundRequest.atAddress == null || !Crypto.isValidAtAddress(refundRequest.atAddress)) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + + Address refundAddress = Address.fromKey(params, refundKey, ScriptType.P2PKH); + + // Extract data from cross-chain trading AT + try (final Repository repository = RepositoryManager.getRepository()) { + ATData atData = fetchAtDataWithChecking(repository, null, refundRequest.atAddress); // null to skip creator check + CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); + + byte[] redeemScriptBytes = BTCACCT.buildScript(refundAddress.getHash(), crossChainTradeData.lockTime, redeemBitcoinAddress.getHash(), crossChainTradeData.secretHash); + byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes); + + Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); + + long now = NTP.getTime(); + + // Check P2SH is funded + final int startTime = (int) (crossChainTradeData.tradeModeTimestamp / 1000L); + List fundingOutputs = new ArrayList<>(); + + Coin p2shBalance = BTC.getInstance().getBalanceAndOtherInfo(p2shAddress.toString(), startTime, fundingOutputs, null); + if (p2shBalance == null) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN); + + if (fundingOutputs.size() != 1) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + + TransactionOutput fundingOutput = fundingOutputs.get(0); + boolean canRefund = now >= crossChainTradeData.lockTime * 1000L; + if (!canRefund) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_TOO_SOON); + + if (p2shBalance.value < crossChainTradeData.expectedBitcoin) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_BALANCE_ISSUE); + + Coin refundAmount = p2shBalance.subtract(Coin.valueOf(refundRequest.bitcoinMinerFee.unscaledValue().longValue())); + + org.bitcoinj.core.Transaction refundTransaction = BTCACCT.buildRefundTransaction(refundAmount, refundKey, fundingOutput, redeemScriptBytes, crossChainTradeData.lockTime); + boolean wasBroadcast = BTC.getInstance().broadcastTransaction(refundTransaction); + + if (!wasBroadcast) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE); + + return refundTransaction.getTxId().toString(); + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + + @POST + @Path("/p2sh/redeem") + @Operation( + summary = "Returns serialized Bitcoin transaction attempting redeem from P2SH address", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = CrossChainBitcoinRedeemRequest.class + ) + ) + ), + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string")) + ) + } + ) + @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN, + ApiError.BTC_TOO_SOON, ApiError.BTC_BALANCE_ISSUE, ApiError.BTC_NETWORK_ISSUE, ApiError.REPOSITORY_ISSUE}) + public String redeemP2sh(CrossChainBitcoinRedeemRequest redeemRequest) { + BTC btc = BTC.getInstance(); + NetworkParameters params = btc.getNetworkParameters(); + + byte[] redeemPrivateKey = redeemRequest.redeemPrivateKey; + if (redeemPrivateKey == null) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY); + + ECKey redeemKey = null; + Address refundBitcoinAddress = null; + + try { + // Auto-trim + if (redeemPrivateKey.length >= 37 && redeemPrivateKey.length <= 38) + redeemPrivateKey = Arrays.copyOfRange(redeemPrivateKey, 1, 33); + if (redeemPrivateKey.length != 32) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY); + + redeemKey = ECKey.fromPrivate(redeemPrivateKey); + } catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + } + + try { + if (redeemRequest.refundAddress == null) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY); + + refundBitcoinAddress = Address.fromString(params, redeemRequest.refundAddress); + if (refundBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + } catch (IllegalArgumentException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + } + + if (redeemRequest.atAddress == null || !Crypto.isValidAtAddress(redeemRequest.atAddress)) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + + if (redeemRequest.secret == null || redeemRequest.secret.length != BTCACCT.SECRET_LENGTH) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); + + Address redeemAddress = Address.fromKey(params, redeemKey, ScriptType.P2PKH); + + // Extract data from cross-chain trading AT + try (final Repository repository = RepositoryManager.getRepository()) { + ATData atData = fetchAtDataWithChecking(repository, null, redeemRequest.atAddress); // null to skip creator check + CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); + + byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), crossChainTradeData.lockTime, redeemAddress.getHash(), crossChainTradeData.secretHash); + byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes); + + Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); + + long medianBlockTime = BTC.getInstance().getMedianBlockTime(); + long now = NTP.getTime(); + + // Check P2SH is funded + final int startTime = (int) (crossChainTradeData.tradeModeTimestamp / 1000L); + List fundingOutputs = new ArrayList<>(); + + Coin p2shBalance = BTC.getInstance().getBalanceAndOtherInfo(p2shAddress.toString(), startTime, fundingOutputs, null); + if (p2shBalance == null) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN); + + if (fundingOutputs.size() != 1) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + + TransactionOutput fundingOutput = fundingOutputs.get(0); + boolean canRedeem = now >= medianBlockTime * 1000L; + if (!canRedeem) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_TOO_SOON); + + if (p2shBalance.value < crossChainTradeData.expectedBitcoin) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_BALANCE_ISSUE); + + Coin redeemAmount = p2shBalance.subtract(Coin.valueOf(redeemRequest.bitcoinMinerFee.unscaledValue().longValue())); + + org.bitcoinj.core.Transaction redeemTransaction = BTCACCT.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutput, redeemScriptBytes, redeemRequest.secret); + boolean wasBroadcast = BTC.getInstance().broadcastTransaction(redeemTransaction); + + if (!wasBroadcast) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE); + + return redeemTransaction.getTxId().toString(); + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + + private ATData fetchAtDataWithChecking(Repository repository, byte[] creatorPublicKey, String atAddress) throws DataException { + ATData atData = repository.getATRepository().fromATAddress(atAddress); + if (atData == null) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN); + + // Does supplied public key match that of AT? + if (creatorPublicKey != null && !Arrays.equals(creatorPublicKey, atData.getCreatorPublicKey())) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY); + + // Must be correct AT - check functionality using code hash + if (!Arrays.equals(atData.getCodeHash(), BTCACCT.CODE_BYTES_HASH)) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + + // No point sending message to AT that's finished + if (atData.getIsFinished()) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + + return atData; + } + + private byte[] buildAtMessage(Repository repository, byte[] senderPublicKey, String atAddress, byte[] messageData) throws DataException { + PublicKeyAccount creatorAccount = new PublicKeyAccount(repository, senderPublicKey); + + long txTimestamp = NTP.getTime(); + byte[] lastReference = creatorAccount.getLastReference(); + + if (lastReference == null) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_REFERENCE); + + Long fee = null; + long amount = 0L; + + BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, senderPublicKey, fee, null); + TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, 4, atAddress, Asset.QORT, amount, messageData, false, false); + + MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData); + + fee = messageTransaction.calcRecommendedFee(); + messageTransactionData.setFee(fee); + + ValidationResult result = messageTransaction.isValidUnconfirmed(); + if (result != ValidationResult.OK) + throw TransactionsResource.createTransactionInvalidException(request, result); + + try { + return MessageTransactionTransformer.toBytes(messageTransactionData); + } catch (TransformationException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e); + } + } + +} \ No newline at end of file diff --git a/src/main/java/org/qortal/at/AT.java b/src/main/java/org/qortal/at/AT.java index 52da2335..cc72184e 100644 --- a/src/main/java/org/qortal/at/AT.java +++ b/src/main/java/org/qortal/at/AT.java @@ -3,6 +3,7 @@ package org.qortal.at; import java.util.List; import org.ciyam.at.MachineState; +import org.ciyam.at.Timestamp; import org.qortal.crypto.Crypto; import org.qortal.data.at.ATData; import org.qortal.data.at.ATStateData; @@ -41,16 +42,25 @@ public class AT { long creation = deployATTransactionData.getTimestamp(); long assetId = deployATTransactionData.getAssetId(); - MachineState machineState = new MachineState(deployATTransactionData.getCreationBytes()); + // Just enough AT data to allow API to query initial balances, etc. + ATData skeletonAtData = new ATData(atAddress, creatorPublicKey, creation, assetId); - this.atData = new ATData(atAddress, creatorPublicKey, creation, machineState.version, assetId, machineState.getCodeBytes(), - machineState.getIsSleeping(), machineState.getSleepUntilHeight(), machineState.getIsFinished(), machineState.getHadFatalError(), - machineState.getIsFrozen(), machineState.getFrozenBalance()); + long blockTimestamp = Timestamp.toLong(height, 0); + QortalATAPI api = new QortalATAPI(repository, skeletonAtData, blockTimestamp); + QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance(); + + MachineState machineState = new MachineState(api, loggerFactory, deployATTransactionData.getCreationBytes()); + + byte[] codeHash = Crypto.digest(machineState.getCodeBytes()); + + this.atData = new ATData(atAddress, creatorPublicKey, creation, machineState.version, assetId, machineState.getCodeBytes(), codeHash, + machineState.isSleeping(), machineState.getSleepUntilHeight(), machineState.isFinished(), machineState.hadFatalError(), + machineState.isFrozen(), machineState.getFrozenBalance()); byte[] stateData = machineState.toBytes(); byte[] stateHash = Crypto.digest(stateData); - this.atStateData = new ATStateData(atAddress, height, creation, stateData, stateHash, 0L); + this.atStateData = new ATStateData(atAddress, height, creation, stateData, stateHash, 0L, true); } // Getters / setters @@ -73,34 +83,85 @@ public class AT { this.repository.getATRepository().delete(this.atData.getATAddress()); } - public List run(long blockTimestamp) throws DataException { + public List run(int blockHeight, long blockTimestamp) throws DataException { String atAddress = this.atData.getATAddress(); QortalATAPI api = new QortalATAPI(repository, this.atData, blockTimestamp); - QortalATLogger logger = new QortalATLogger(); + QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance(); byte[] codeBytes = this.atData.getCodeBytes(); - // Fetch latest ATStateData for this AT (if any) + // Fetch latest ATStateData for this AT ATStateData latestAtStateData = this.repository.getATRepository().getLatestATState(atAddress); - // There should be at least initial AT state data + // There should be at least initial deployment AT state data if (latestAtStateData == null) - throw new IllegalStateException("No initial AT state data found"); + throw new IllegalStateException("No previous AT state data found"); // [Re]create AT machine state using AT state data or from scratch as applicable - MachineState state = MachineState.fromBytes(api, logger, latestAtStateData.getStateData(), codeBytes); + MachineState state = MachineState.fromBytes(api, loggerFactory, latestAtStateData.getStateData(), codeBytes); state.execute(); - int height = this.repository.getBlockRepository().getBlockchainHeight() + 1; long creation = this.atData.getCreation(); byte[] stateData = state.toBytes(); byte[] stateHash = Crypto.digest(stateData); long atFees = api.calcFinalFees(state); - this.atStateData = new ATStateData(atAddress, height, creation, stateData, stateHash, atFees); + this.atStateData = new ATStateData(atAddress, blockHeight, creation, stateData, stateHash, atFees, false); return api.getTransactions(); } + public void update(int blockHeight, long blockTimestamp) throws DataException { + // [Re]create AT machine state using AT state data or from scratch as applicable + QortalATAPI api = new QortalATAPI(repository, this.atData, blockTimestamp); + QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance(); + + byte[] codeBytes = this.atData.getCodeBytes(); + MachineState state = MachineState.fromBytes(api, loggerFactory, this.atStateData.getStateData(), codeBytes); + + // Save latest AT state data + this.repository.getATRepository().save(this.atStateData); + + // Update AT info in repository too + this.atData.setIsSleeping(state.isSleeping()); + this.atData.setSleepUntilHeight(state.getSleepUntilHeight()); + this.atData.setIsFinished(state.isFinished()); + this.atData.setHadFatalError(state.hadFatalError()); + this.atData.setIsFrozen(state.isFrozen()); + this.atData.setFrozenBalance(state.getFrozenBalance()); + this.repository.getATRepository().save(this.atData); + } + + public void revert(int blockHeight, long blockTimestamp) throws DataException { + String atAddress = this.atData.getATAddress(); + + // Delete old AT state data from repository + this.repository.getATRepository().delete(atAddress, blockHeight); + + if (this.atStateData.isInitial()) + return; + + // Load previous state data + ATStateData previousStateData = this.repository.getATRepository().getLatestATState(atAddress); + if (previousStateData == null) + throw new DataException("Can't find previous AT state data for " + atAddress); + + // [Re]create AT machine state using AT state data or from scratch as applicable + QortalATAPI api = new QortalATAPI(repository, this.atData, blockTimestamp); + QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance(); + + byte[] codeBytes = this.atData.getCodeBytes(); + MachineState state = MachineState.fromBytes(api, loggerFactory, previousStateData.getStateData(), codeBytes); + + // Update AT info in repository + this.atData.setIsSleeping(state.isSleeping()); + this.atData.setSleepUntilHeight(state.getSleepUntilHeight()); + this.atData.setIsFinished(state.isFinished()); + this.atData.setHadFatalError(state.hadFatalError()); + this.atData.setIsFrozen(state.isFrozen()); + this.atData.setFrozenBalance(state.getFrozenBalance()); + this.repository.getATRepository().save(this.atData); + } + } diff --git a/src/main/java/org/qortal/at/BlockchainAPI.java b/src/main/java/org/qortal/at/BlockchainAPI.java deleted file mode 100644 index e21d786d..00000000 --- a/src/main/java/org/qortal/at/BlockchainAPI.java +++ /dev/null @@ -1,133 +0,0 @@ -package org.qortal.at; - -import static java.util.Arrays.stream; -import static java.util.stream.Collectors.toMap; - -import java.util.List; -import java.util.Map; - -import org.ciyam.at.MachineState; -import org.ciyam.at.Timestamp; -import org.qortal.account.Account; -import org.qortal.block.Block; -import org.qortal.data.block.BlockData; -import org.qortal.data.transaction.ATTransactionData; -import org.qortal.data.transaction.PaymentTransactionData; -import org.qortal.data.transaction.TransactionData; -import org.qortal.repository.BlockRepository; -import org.qortal.repository.DataException; -import org.qortal.transaction.Transaction; - -public enum BlockchainAPI { - - QORTAL(0) { - @Override - public void putTransactionFromRecipientAfterTimestampInA(String recipient, Timestamp timestamp, MachineState state) { - int height = timestamp.blockHeight; - int sequence = timestamp.transactionSequence + 1; - - QortalATAPI api = (QortalATAPI) state.getAPI(); - BlockRepository blockRepository = api.repository.getBlockRepository(); - - try { - Account recipientAccount = new Account(api.repository, recipient); - - while (height <= blockRepository.getBlockchainHeight()) { - BlockData blockData = blockRepository.fromHeight(height); - - if (blockData == null) - throw new DataException("Unable to fetch block " + height + " from repository?"); - - Block block = new Block(api.repository, blockData); - - List transactions = block.getTransactions(); - - // No more transactions in this block? Try next block - if (sequence >= transactions.size()) { - ++height; - sequence = 0; - continue; - } - - Transaction transaction = transactions.get(sequence); - - // Transaction needs to be sent to specified recipient - if (transaction.getRecipientAccounts().contains(recipientAccount)) { - // Found a transaction - - api.setA1(state, new Timestamp(height, timestamp.blockchainId, sequence).longValue()); - - // Hash transaction's signature into other three A fields for future verification that it's the same transaction - byte[] hash = QortalATAPI.sha192(transaction.getTransactionData().getSignature()); - - api.setA2(state, QortalATAPI.fromBytes(hash, 0)); - api.setA3(state, QortalATAPI.fromBytes(hash, 8)); - api.setA4(state, QortalATAPI.fromBytes(hash, 16)); - return; - } - - // Transaction wasn't for us - keep going - ++sequence; - } - - // No more transactions - zero A and exit - api.zeroA(state); - } catch (DataException e) { - throw new RuntimeException("AT API unable to fetch next transaction?", e); - } - } - - @Override - public long getAmountFromTransactionInA(Timestamp timestamp, MachineState state) { - QortalATAPI api = (QortalATAPI) state.getAPI(); - TransactionData transactionData = api.fetchTransaction(state); - - switch (transactionData.getType()) { - case PAYMENT: - return ((PaymentTransactionData) transactionData).getAmount(); - - case AT: - Long amount = ((ATTransactionData) transactionData).getAmount(); - - if (amount == null) - return 0xffffffffffffffffL; - - return amount; - - default: - return 0xffffffffffffffffL; - } - } - }, - BTC(1) { - @Override - public void putTransactionFromRecipientAfterTimestampInA(String recipient, Timestamp timestamp, MachineState state) { - // TODO BTC transaction support for ATv2 - } - - @Override - public long getAmountFromTransactionInA(Timestamp timestamp, MachineState state) { - // TODO BTC transaction support for ATv2 - return 0; - } - }; - - public final int value; - - private static final Map map = stream(BlockchainAPI.values()).collect(toMap(type -> type.value, type -> type)); - - BlockchainAPI(int value) { - this.value = value; - } - - public static BlockchainAPI valueOf(int value) { - return map.get(value); - } - - // Blockchain-specific API methods - - public abstract void putTransactionFromRecipientAfterTimestampInA(String recipient, Timestamp timestamp, MachineState state); - - public abstract long getAmountFromTransactionInA(Timestamp timestamp, MachineState state); - -} diff --git a/src/main/java/org/qortal/at/QortalATAPI.java b/src/main/java/org/qortal/at/QortalATAPI.java index 91ba06f3..9ed6fdb6 100644 --- a/src/main/java/org/qortal/at/QortalATAPI.java +++ b/src/main/java/org/qortal/at/QortalATAPI.java @@ -1,10 +1,12 @@ package org.qortal.at; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.ciyam.at.API; import org.ciyam.at.ExecutionException; import org.ciyam.at.FunctionData; @@ -16,33 +18,39 @@ import org.qortal.account.Account; import org.qortal.account.NullAccount; import org.qortal.account.PublicKeyAccount; import org.qortal.asset.Asset; +import org.qortal.block.Block; +import org.qortal.block.BlockChain; +import org.qortal.block.BlockChain.CiyamAtSettings; import org.qortal.crypto.Crypto; import org.qortal.data.at.ATData; import org.qortal.data.block.BlockData; +import org.qortal.data.block.BlockSummaryData; import org.qortal.data.transaction.ATTransactionData; import org.qortal.data.transaction.BaseTransactionData; import org.qortal.data.transaction.MessageTransactionData; +import org.qortal.data.transaction.PaymentTransactionData; import org.qortal.data.transaction.TransactionData; import org.qortal.group.Group; +import org.qortal.repository.BlockRepository; import org.qortal.repository.DataException; import org.qortal.repository.Repository; import org.qortal.transaction.AtTransaction; -import org.qortal.utils.Amounts; +import org.qortal.transaction.Transaction; +import org.qortal.transaction.Transaction.TransactionType; +import org.qortal.utils.Base58; import com.google.common.primitives.Bytes; public class QortalATAPI extends API { - // Useful constants - private static final long FEE_PER_STEP = 1 * Amounts.MULTIPLIER; // 1 QORT per "step" - private static final int MAX_STEPS_PER_ROUND = 500; - private static final int STEPS_PER_FUNCTION_CALL = 10; - private static final int MINUTES_PER_BLOCK = 10; + private static final byte[] ADDRESS_PADDING = new byte[32 - Account.ADDRESS_LENGTH]; + private static final Logger LOGGER = LogManager.getLogger(QortalATAPI.class); // Properties - Repository repository; - ATData atData; - long blockTimestamp; + private Repository repository; + private ATData atData; + private long blockTimestamp; + private final CiyamAtSettings ciyamAtSettings; /** List of generated AT transactions */ List transactions; @@ -54,36 +62,42 @@ public class QortalATAPI extends API { this.atData = atData; this.transactions = new ArrayList<>(); this.blockTimestamp = blockTimestamp; + + this.ciyamAtSettings = BlockChain.getInstance().getCiyamAtSettings(); } // Methods specific to Qortal AT processing, not inherited + public Repository getRepository() { + return this.repository; + } + public List getTransactions() { return this.transactions; } public long calcFinalFees(MachineState state) { - return state.getSteps() * FEE_PER_STEP; + return state.getSteps() * this.ciyamAtSettings.feePerStep; } // Inherited methods from CIYAM AT API @Override public int getMaxStepsPerRound() { - return MAX_STEPS_PER_ROUND; + return this.ciyamAtSettings.maxStepsPerRound; } @Override public int getOpCodeSteps(OpCode opcode) { if (opcode.value >= OpCode.EXT_FUN.value && opcode.value <= OpCode.EXT_FUN_RET_DAT_2.value) - return STEPS_PER_FUNCTION_CALL; + return this.ciyamAtSettings.stepsPerFunctionCall; return 1; } @Override public long getFeePerStep() { - return FEE_PER_STEP; + return this.ciyamAtSettings.feePerStep; } @Override @@ -105,31 +119,96 @@ public class QortalATAPI extends API { } @Override - public void putPreviousBlockHashInA(MachineState state) { + public void putPreviousBlockHashIntoA(MachineState state) { try { - BlockData blockData = this.repository.getBlockRepository().fromHeight(this.getPreviousBlockHeight()); + int previousBlockHeight = this.repository.getBlockRepository().getBlockchainHeight() - 1; + + // We only need signature, so only request a block summary + List blockSummaries = this.repository.getBlockRepository().getBlockSummaries(previousBlockHeight, previousBlockHeight); + if (blockSummaries == null || blockSummaries.size() != 1) + throw new RuntimeException("AT API unable to fetch previous block hash?"); // Block's signature is 128 bytes so we need to reduce this to 4 longs (32 bytes) - byte[] blockHash = Crypto.digest(blockData.getSignature()); + // To be able to use hash to look up block, save height (8 bytes) and partial signature (24 bytes) + this.setA1(state, previousBlockHeight); - this.setA(state, blockHash); + byte[] signature = blockSummaries.get(0).getSignature(); + // Save some of minter's signature and transactions signature, so middle 24 bytes of the full 128 byte signature. + this.setA2(state, fromBytes(signature, 52)); + this.setA3(state, fromBytes(signature, 60)); + this.setA4(state, fromBytes(signature, 68)); } catch (DataException e) { throw new RuntimeException("AT API unable to fetch previous block?", e); } } @Override - public void putTransactionAfterTimestampInA(Timestamp timestamp, MachineState state) { + public void putTransactionAfterTimestampIntoA(Timestamp timestamp, MachineState state) { // Recipient is this AT - String recipient = this.atData.getATAddress(); + String atAddress = this.atData.getATAddress(); - BlockchainAPI blockchainAPI = BlockchainAPI.valueOf(timestamp.blockchainId); - blockchainAPI.putTransactionFromRecipientAfterTimestampInA(recipient, timestamp, state); + int height = timestamp.blockHeight; + int sequence = timestamp.transactionSequence + 1; + + BlockRepository blockRepository = this.getRepository().getBlockRepository(); + + try { + int currentHeight = blockRepository.getBlockchainHeight(); + List blockTransactions = null; + + while (height <= currentHeight) { + if (blockTransactions == null) { + BlockData blockData = blockRepository.fromHeight(height); + + if (blockData == null) + throw new DataException("Unable to fetch block " + height + " from repository?"); + + Block block = new Block(this.getRepository(), blockData); + + blockTransactions = block.getTransactions(); + } + + // No more transactions in this block? Try next block + if (sequence >= blockTransactions.size()) { + ++height; + sequence = 0; + blockTransactions = null; + continue; + } + + Transaction transaction = blockTransactions.get(sequence); + + // Transaction needs to be sent to specified recipient + List recipientAccounts = transaction.getRecipientAccounts(); + List recipientAddresses = recipientAccounts.stream().map(Account::getAddress).collect(Collectors.toList()); + if (recipientAddresses.contains(atAddress)) { + // Found a transaction + + this.setA1(state, new Timestamp(height, timestamp.blockchainId, sequence).longValue()); + + // Copy transaction's partial signature into the other three A fields for future verification that it's the same transaction + byte[] signature = transaction.getTransactionData().getSignature(); + this.setA2(state, fromBytes(signature, 8)); + this.setA3(state, fromBytes(signature, 16)); + this.setA4(state, fromBytes(signature, 24)); + + return; + } + + // Transaction wasn't for us - keep going + ++sequence; + } + + // No more transactions - zero A and exit + this.zeroA(state); + } catch (DataException e) { + throw new RuntimeException("AT API unable to fetch next transaction?", e); + } } @Override public long getTypeFromTransactionInA(MachineState state) { - TransactionData transactionData = this.fetchTransaction(state); + TransactionData transactionData = this.getTransactionFromA(state); switch (transactionData.getType()) { case PAYMENT: @@ -151,22 +230,36 @@ public class QortalATAPI extends API { @Override public long getAmountFromTransactionInA(MachineState state) { - Timestamp timestamp = new Timestamp(state.getA1()); - BlockchainAPI blockchainAPI = BlockchainAPI.valueOf(timestamp.blockchainId); - return blockchainAPI.getAmountFromTransactionInA(timestamp, state); + TransactionData transactionData = this.getTransactionFromA(state); + + switch (transactionData.getType()) { + case PAYMENT: + return ((PaymentTransactionData) transactionData).getAmount(); + + case AT: + Long amount = ((ATTransactionData) transactionData).getAmount(); + + if (amount != null) + return amount; + + // fall-through to default + + default: + return 0xffffffffffffffffL; + } } @Override public long getTimestampFromTransactionInA(MachineState state) { // Transaction's "timestamp" already stored in A1 - Timestamp timestamp = new Timestamp(state.getA1()); + Timestamp timestamp = new Timestamp(this.getA1(state)); return timestamp.longValue(); } @Override public long generateRandomUsingTransactionInA(MachineState state) { - // The plan here is to sleep for a block then use next block's signature and this transaction's signature to generate pseudo-random, but deterministic, - // value. + // The plan here is to sleep for a block then use next block's signature + // and this transaction's signature to generate pseudo-random, but deterministic, value. if (!isFirstOpCodeAfterSleeping(state)) { // First call @@ -179,7 +272,7 @@ public class QortalATAPI extends API { // Second call // HASH(A and new block hash) - TransactionData transactionData = this.fetchTransaction(state); + TransactionData transactionData = this.getTransactionFromA(state); try { BlockData blockData = this.repository.getBlockRepository().getLastBlock(); @@ -203,7 +296,7 @@ public class QortalATAPI extends API { // Zero B in case of issues or shorter-than-B message this.zeroB(state); - TransactionData transactionData = this.fetchTransaction(state); + TransactionData transactionData = this.getTransactionFromA(state); byte[] messageData = null; @@ -233,20 +326,36 @@ public class QortalATAPI extends API { @Override public void putAddressFromTransactionInAIntoB(MachineState state) { - TransactionData transactionData = this.fetchTransaction(state); + TransactionData transactionData = this.getTransactionFromA(state); - // We actually use public key as it has more potential utility (e.g. message verification) than an address - byte[] bytes = transactionData.getCreatorPublicKey(); + String address; + if (transactionData.getType() == TransactionType.AT) { + // Use AT address from transaction data, as transaction's public key will always be fake + address = ((ATTransactionData) transactionData).getATAddress(); + } else { + byte[] publicKey = transactionData.getCreatorPublicKey(); + address = Crypto.toAddress(publicKey); + } - this.setB(state, bytes); + // Convert to byte form as this only takes 25 bytes, + // compared to string-form's 34 bytes, + // and we only have 32 bytes available. + byte[] addressBytes = Bytes.ensureCapacity(Base58.decode(address), 32, 0); // pad to 32 bytes + + this.setB(state, addressBytes); } @Override public void putCreatorAddressIntoB(MachineState state) { - // We actually use public key as it has more potential utility (e.g. message verification) than an address - byte[] bytes = atData.getCreatorPublicKey(); + byte[] publicKey = atData.getCreatorPublicKey(); + String address = Crypto.toAddress(publicKey); - this.setB(state, bytes); + // Convert to byte form as this only takes 25 bytes, + // compared to string-form's 34 bytes, + // and we only have 32 bytes available. + byte[] addressBytes = Bytes.ensureCapacity(Base58.decode(address), 32, 0); // pad to 32 bytes + + this.setB(state, addressBytes); } @Override @@ -262,9 +371,7 @@ public class QortalATAPI extends API { @Override public void payAmountToB(long amount, MachineState state) { - byte[] publicKey = state.getB(); - - PublicKeyAccount recipient = new PublicKeyAccount(this.repository, publicKey); + Account recipient = getAccountFromB(state); long timestamp = this.getNextTransactionTimestamp(); byte[] reference = this.getLastReference(); @@ -280,10 +387,8 @@ public class QortalATAPI extends API { @Override public void messageAToB(MachineState state) { - byte[] message = state.getA(); - byte[] publicKey = state.getB(); - - PublicKeyAccount recipient = new PublicKeyAccount(this.repository, publicKey); + byte[] message = this.getA(state); + Account recipient = getAccountFromB(state); long timestamp = this.getNextTransactionTimestamp(); byte[] reference = this.getLastReference(); @@ -302,13 +407,16 @@ public class QortalATAPI extends API { int blockHeight = timestamp.blockHeight; // At least one block in the future - blockHeight += (minutes / MINUTES_PER_BLOCK) + 1; + blockHeight += Math.max(minutes / this.ciyamAtSettings.minutesPerBlock, 1); return new Timestamp(blockHeight, 0).longValue(); } @Override public void onFinished(long finalBalance, MachineState state) { + if (finalBalance <= 0) + return; + // Refund remaining balance (if any) to AT's creator Account creator = this.getCreator(); long timestamp = this.getNextTransactionTimestamp(); @@ -325,7 +433,7 @@ public class QortalATAPI extends API { @Override public void onFatalError(MachineState state, ExecutionException e) { - state.getLogger().error("AT " + this.atData.getATAddress() + " suffered fatal error: " + e.getMessage()); + LOGGER.error("AT " + this.atData.getATAddress() + " suffered fatal error: " + e.getMessage()); } @Override @@ -336,7 +444,7 @@ public class QortalATAPI extends API { if (qortalFunctionCode == null) throw new IllegalFunctionCodeException("Unknown Qortal function code 0x" + String.format("%04x", rawFunctionCode) + " encountered"); - qortalFunctionCode.preExecuteCheck(2, true, state, rawFunctionCode); + qortalFunctionCode.preExecuteCheck(paramCount, returnValueExpected, rawFunctionCode); } @Override @@ -354,29 +462,23 @@ public class QortalATAPI extends API { | (bytes[start + 4] & 0xffL) << 32 | (bytes[start + 5] & 0xffL) << 40 | (bytes[start + 6] & 0xffL) << 48 | (bytes[start + 7] & 0xffL) << 56; } - /** Returns SHA2-192 digest of input - used to verify transaction signatures */ - public static byte[] sha192(byte[] input) { - try { - // SHA2-192 - MessageDigest sha192 = MessageDigest.getInstance("SHA-192"); - return sha192.digest(input); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException("SHA-192 not available"); - } + /** 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); } - /** Verify transaction's SHA2-192 hashed signature matches A2 thru A4 */ - private static void verifyTransaction(TransactionData transactionData, MachineState state) { - // Compare SHA2-192 of transaction's signature against A2 thru A4 - byte[] hash = sha192(transactionData.getSignature()); + /** Verify transaction's partial signature matches A2 thru A4 */ + private void verifyTransaction(TransactionData transactionData, MachineState state) { + // Compare end of transaction's signature against A2 thru A4 + byte[] sig = transactionData.getSignature(); - if (state.getA2() != fromBytes(hash, 0) || state.getA3() != fromBytes(hash, 8) || state.getA4() != fromBytes(hash, 16)) + if (this.getA2(state) != fromBytes(sig, 8) || this.getA3(state) != fromBytes(sig, 16) || this.getA4(state) != fromBytes(sig, 24)) throw new IllegalStateException("Transaction signature in A no longer matches signature from repository"); } /** Returns transaction data from repository using block height & sequence from A1, checking the transaction signatures match too */ - /* package */ TransactionData fetchTransaction(MachineState state) { - Timestamp timestamp = new Timestamp(state.getA1()); + /* package */ TransactionData getTransactionFromA(MachineState state) { + Timestamp timestamp = new Timestamp(this.getA1(state)); try { TransactionData transactionData = this.repository.getTransactionRepository().fromHeightAndSequence(timestamp.blockHeight, @@ -407,19 +509,11 @@ public class QortalATAPI extends API { /** Returns the timestamp to use for next AT Transaction */ private long getNextTransactionTimestamp() { /* - * Timestamp is block's timestamp + position in AT-Transactions list. + * Use block's timestamp. * - * We need increasing timestamps to preserve transaction order and hence a correct signature-reference chain when the block is processed. - * - * As Qora blocks must share the same milliseconds component in their timestamps, this allows us to generate up to 1,000 AT-Transactions per AT without - * issue. - * - * As long as ATs are not allowed to generate that many per block, e.g. by limiting maximum steps per execution round, then we should be fine. + * This is OK because AT transactions are always generated locally and order is preserved in Transaction.getDataComparator(). */ - - // XXX THE ABOVE IS NO LONGER TRUE IN QORTAL! - // return this.blockTimestamp + this.transactions.size(); - throw new RuntimeException("AT timestamp code not fixed!"); + return this.blockTimestamp; } /** Returns AT account's lastReference, taking newly generated ATTransactions into account */ @@ -438,4 +532,38 @@ public class QortalATAPI extends API { } } + /** + * Returns Account (possibly PublicKeyAccount) based on value in B. + *

+ * If first byte in B starts with either address version bytes,
+ * and bytes 26 to 32 are zero, then use as an address, but only if valid. + *

+ * Otherwise, assume B is a public key. + */ + private Account getAccountFromB(MachineState state) { + byte[] bBytes = this.getB(state); + + if ((bBytes[0] == Crypto.ADDRESS_VERSION || bBytes[0] == Crypto.AT_ADDRESS_VERSION) + && Arrays.mismatch(bBytes, Account.ADDRESS_LENGTH, 32, ADDRESS_PADDING, 0, ADDRESS_PADDING.length) == -1) { + // Extract only the bytes containing address + byte[] addressBytes = Arrays.copyOf(bBytes, Account.ADDRESS_LENGTH); + // If address (in byte form) is valid... + if (Crypto.isValidAddress(addressBytes)) + // ...then return an Account using address (converted to Base58 + return new Account(this.repository, Base58.encode(addressBytes)); + } + + return new PublicKeyAccount(this.repository, bBytes); + } + + /* Convenience methods to allow QortalFunctionCode package-visibility access to A/B-get/set methods. */ + + protected byte[] getB(MachineState state) { + return super.getB(state); + } + + protected void setB(MachineState state, byte[] bBytes) { + super.setB(state, bBytes); + } + } diff --git a/src/main/java/org/qortal/at/QortalATLogger.java b/src/main/java/org/qortal/at/QortalATLogger.java deleted file mode 100644 index df01247a..00000000 --- a/src/main/java/org/qortal/at/QortalATLogger.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.qortal.at; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -public class QortalATLogger implements org.ciyam.at.LoggerInterface { - - // NOTE: We're logging on behalf of org.qortal.at.AT, not ourselves! - private static final Logger LOGGER = LogManager.getLogger(AT.class); - - @Override - public void error(String message) { - LOGGER.error(message); - } - - @Override - public void debug(String message) { - LOGGER.debug(message); - } - - @Override - public void echo(String message) { - LOGGER.info(message); - } - -} diff --git a/src/main/java/org/qortal/at/QortalAtLogger.java b/src/main/java/org/qortal/at/QortalAtLogger.java new file mode 100644 index 00000000..703972a6 --- /dev/null +++ b/src/main/java/org/qortal/at/QortalAtLogger.java @@ -0,0 +1,2182 @@ +package org.qortal.at; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.message.Message; +import org.apache.logging.log4j.message.MessageFactory; +import org.apache.logging.log4j.spi.AbstractLogger; +import org.apache.logging.log4j.spi.ExtendedLoggerWrapper; +import org.apache.logging.log4j.util.MessageSupplier; +import org.apache.logging.log4j.util.Supplier; + +/** + * Extended Logger interface with convenience methods for + * the ERROR, DEBUG and ECHO custom log levels. + *

Compatible with Log4j 2.6 or higher.

+ */ +public final class QortalAtLogger extends ExtendedLoggerWrapper implements org.ciyam.at.AtLogger { + private static final long serialVersionUID = 4740841485167L; + private final ExtendedLoggerWrapper logger; + + private static final String FQCN = QortalAtLogger.class.getName(); + private static final Level ERROR = Level.forName("ERROR", 200); + private static final Level DEBUG = Level.forName("DEBUG", 500); + private static final Level ECHO = Level.forName("ECHO", 400); + + private QortalAtLogger(final Logger logger) { + super((AbstractLogger) logger, logger.getName(), logger.getMessageFactory()); + this.logger = this; + } + + /** + * Returns a custom Logger with the name of the calling class. + * + * @return The custom Logger for the calling class. + */ + public static QortalAtLogger create() { + final Logger wrapped = LogManager.getLogger(); + return new QortalAtLogger(wrapped); + } + + /** + * Returns a custom Logger using the fully qualified name of the Class as + * the Logger name. + * + * @param loggerName The Class whose name should be used as the Logger name. + * If null it will default to the calling class. + * @return The custom Logger. + */ + public static QortalAtLogger create(final Class loggerName) { + final Logger wrapped = LogManager.getLogger(loggerName); + return new QortalAtLogger(wrapped); + } + + /** + * Returns a custom Logger using the fully qualified name of the Class as + * the Logger name. + * + * @param loggerName The Class whose name should be used as the Logger name. + * If null it will default to the calling class. + * @param messageFactory The message factory is used only when creating a + * logger, subsequent use does not change the logger but will log + * a warning if mismatched. + * @return The custom Logger. + */ + public static QortalAtLogger create(final Class loggerName, final MessageFactory messageFactory) { + final Logger wrapped = LogManager.getLogger(loggerName, messageFactory); + return new QortalAtLogger(wrapped); + } + + /** + * Returns a custom Logger using the fully qualified class name of the value + * as the Logger name. + * + * @param value The value whose class name should be used as the Logger + * name. If null the name of the calling class will be used as + * the logger name. + * @return The custom Logger. + */ + public static QortalAtLogger create(final Object value) { + final Logger wrapped = LogManager.getLogger(value); + return new QortalAtLogger(wrapped); + } + + /** + * Returns a custom Logger using the fully qualified class name of the value + * as the Logger name. + * + * @param value The value whose class name should be used as the Logger + * name. If null the name of the calling class will be used as + * the logger name. + * @param messageFactory The message factory is used only when creating a + * logger, subsequent use does not change the logger but will log + * a warning if mismatched. + * @return The custom Logger. + */ + public static QortalAtLogger create(final Object value, final MessageFactory messageFactory) { + final Logger wrapped = LogManager.getLogger(value, messageFactory); + return new QortalAtLogger(wrapped); + } + + /** + * Returns a custom Logger with the specified name. + * + * @param name The logger name. If null the name of the calling class will + * be used. + * @return The custom Logger. + */ + public static QortalAtLogger create(final String name) { + final Logger wrapped = LogManager.getLogger(name); + return new QortalAtLogger(wrapped); + } + + /** + * Returns a custom Logger with the specified name. + * + * @param name The logger name. If null the name of the calling class will + * be used. + * @param messageFactory The message factory is used only when creating a + * logger, subsequent use does not change the logger but will log + * a warning if mismatched. + * @return The custom Logger. + */ + public static QortalAtLogger create(final String name, final MessageFactory messageFactory) { + final Logger wrapped = LogManager.getLogger(name, messageFactory); + return new QortalAtLogger(wrapped); + } + + /** + * Logs a message with the specific Marker at the {@code ERROR} level. + * + * @param marker the marker data specific to this log statement + * @param msg the message string to be logged + */ + public void error(final Marker marker, final Message msg) { + logger.logIfEnabled(FQCN, ERROR, marker, msg, (Throwable) null); + } + + /** + * Logs a message with the specific Marker at the {@code ERROR} level. + * + * @param marker the marker data specific to this log statement + * @param msg the message string to be logged + * @param t A Throwable or null. + */ + public void error(final Marker marker, final Message msg, final Throwable t) { + logger.logIfEnabled(FQCN, ERROR, marker, msg, t); + } + + /** + * Logs a message object with the {@code ERROR} level. + * + * @param marker the marker data specific to this log statement + * @param message the message object to log. + */ + public void error(final Marker marker, final Object message) { + logger.logIfEnabled(FQCN, ERROR, marker, message, (Throwable) null); + } + + /** + * Logs a message CharSequence with the {@code ERROR} level. + * + * @param marker the marker data specific to this log statement + * @param message the message CharSequence to log. + * @since Log4j-2.6 + */ + public void error(final Marker marker, final CharSequence message) { + logger.logIfEnabled(FQCN, ERROR, marker, message, (Throwable) null); + } + + /** + * Logs a message at the {@code ERROR} level including the stack trace of + * the {@link Throwable} {@code t} passed as parameter. + * + * @param marker the marker data specific to this log statement + * @param message the message to log. + * @param t the exception to log, including its stack trace. + */ + public void error(final Marker marker, final Object message, final Throwable t) { + logger.logIfEnabled(FQCN, ERROR, marker, message, t); + } + + /** + * Logs a message at the {@code ERROR} level including the stack trace of + * the {@link Throwable} {@code t} passed as parameter. + * + * @param marker the marker data specific to this log statement + * @param message the CharSequence to log. + * @param t the exception to log, including its stack trace. + * @since Log4j-2.6 + */ + public void error(final Marker marker, final CharSequence message, final Throwable t) { + logger.logIfEnabled(FQCN, ERROR, marker, message, t); + } + + /** + * Logs a message object with the {@code ERROR} level. + * + * @param marker the marker data specific to this log statement + * @param message the message object to log. + */ + public void error(final Marker marker, final String message) { + logger.logIfEnabled(FQCN, ERROR, marker, message, (Throwable) null); + } + + /** + * Logs a message with parameters at the {@code ERROR} level. + * + * @param marker the marker data specific to this log statement + * @param message the message to log; the format depends on the message factory. + * @param params parameters to the message. + * @see #getMessageFactory() + */ + public void error(final Marker marker, final String message, final Object... params) { + logger.logIfEnabled(FQCN, ERROR, marker, message, params); + } + + /** + * Logs a message with parameters at the {@code ERROR} level. + * + * @param marker the marker data specific to this log statement + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void error(final Marker marker, final String message, final Object p0) { + logger.logIfEnabled(FQCN, ERROR, marker, message, p0); + } + + /** + * Logs a message with parameters at the {@code ERROR} level. + * + * @param marker the marker data specific to this log statement + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void error(final Marker marker, final String message, final Object p0, final Object p1) { + logger.logIfEnabled(FQCN, ERROR, marker, message, p0, p1); + } + + /** + * Logs a message with parameters at the {@code ERROR} level. + * + * @param marker the marker data specific to this log statement + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void error(final Marker marker, final String message, final Object p0, final Object p1, final Object p2) { + logger.logIfEnabled(FQCN, ERROR, marker, message, p0, p1, p2); + } + + /** + * Logs a message with parameters at the {@code ERROR} level. + * + * @param marker the marker data specific to this log statement + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void error(final Marker marker, final String message, final Object p0, final Object p1, final Object p2, + final Object p3) { + logger.logIfEnabled(FQCN, ERROR, marker, message, p0, p1, p2, p3); + } + + /** + * Logs a message with parameters at the {@code ERROR} level. + * + * @param marker the marker data specific to this log statement + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @param p4 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void error(final Marker marker, final String message, final Object p0, final Object p1, final Object p2, + final Object p3, final Object p4) { + logger.logIfEnabled(FQCN, ERROR, marker, message, p0, p1, p2, p3, p4); + } + + /** + * Logs a message with parameters at the {@code ERROR} level. + * + * @param marker the marker data specific to this log statement + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @param p4 parameter to the message. + * @param p5 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void error(final Marker marker, final String message, final Object p0, final Object p1, final Object p2, + final Object p3, final Object p4, final Object p5) { + logger.logIfEnabled(FQCN, ERROR, marker, message, p0, p1, p2, p3, p4, p5); + } + + /** + * Logs a message with parameters at the {@code ERROR} level. + * + * @param marker the marker data specific to this log statement + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @param p4 parameter to the message. + * @param p5 parameter to the message. + * @param p6 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void error(final Marker marker, final String message, final Object p0, final Object p1, final Object p2, + final Object p3, final Object p4, final Object p5, final Object p6) { + logger.logIfEnabled(FQCN, ERROR, marker, message, p0, p1, p2, p3, p4, p5, p6); + } + + /** + * Logs a message with parameters at the {@code ERROR} level. + * + * @param marker the marker data specific to this log statement + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @param p4 parameter to the message. + * @param p5 parameter to the message. + * @param p6 parameter to the message. + * @param p7 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void error(final Marker marker, final String message, final Object p0, final Object p1, final Object p2, + final Object p3, final Object p4, final Object p5, final Object p6, + final Object p7) { + logger.logIfEnabled(FQCN, ERROR, marker, message, p0, p1, p2, p3, p4, p5, p6, p7); + } + + /** + * Logs a message with parameters at the {@code ERROR} level. + * + * @param marker the marker data specific to this log statement + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @param p4 parameter to the message. + * @param p5 parameter to the message. + * @param p6 parameter to the message. + * @param p7 parameter to the message. + * @param p8 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void error(final Marker marker, final String message, final Object p0, final Object p1, final Object p2, + final Object p3, final Object p4, final Object p5, final Object p6, + final Object p7, final Object p8) { + logger.logIfEnabled(FQCN, ERROR, marker, message, p0, p1, p2, p3, p4, p5, p6, p7, p8); + } + + /** + * Logs a message with parameters at the {@code ERROR} level. + * + * @param marker the marker data specific to this log statement + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @param p4 parameter to the message. + * @param p5 parameter to the message. + * @param p6 parameter to the message. + * @param p7 parameter to the message. + * @param p8 parameter to the message. + * @param p9 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void error(final Marker marker, final String message, final Object p0, final Object p1, final Object p2, + final Object p3, final Object p4, final Object p5, final Object p6, + final Object p7, final Object p8, final Object p9) { + logger.logIfEnabled(FQCN, ERROR, marker, message, p0, p1, p2, p3, p4, p5, p6, p7, p8, p9); + } + + /** + * Logs a message at the {@code ERROR} level including the stack trace of + * the {@link Throwable} {@code t} passed as parameter. + * + * @param marker the marker data specific to this log statement + * @param message the message to log. + * @param t the exception to log, including its stack trace. + */ + public void error(final Marker marker, final String message, final Throwable t) { + logger.logIfEnabled(FQCN, ERROR, marker, message, t); + } + + /** + * Logs the specified Message at the {@code ERROR} level. + * + * @param msg the message string to be logged + */ + public void error(final Message msg) { + logger.logIfEnabled(FQCN, ERROR, null, msg, (Throwable) null); + } + + /** + * Logs the specified Message at the {@code ERROR} level. + * + * @param msg the message string to be logged + * @param t A Throwable or null. + */ + public void error(final Message msg, final Throwable t) { + logger.logIfEnabled(FQCN, ERROR, null, msg, t); + } + + /** + * Logs a message object with the {@code ERROR} level. + * + * @param message the message object to log. + */ + public void error(final Object message) { + logger.logIfEnabled(FQCN, ERROR, null, message, (Throwable) null); + } + + /** + * Logs a message at the {@code ERROR} level including the stack trace of + * the {@link Throwable} {@code t} passed as parameter. + * + * @param message the message to log. + * @param t the exception to log, including its stack trace. + */ + public void error(final Object message, final Throwable t) { + logger.logIfEnabled(FQCN, ERROR, null, message, t); + } + + /** + * Logs a message CharSequence with the {@code ERROR} level. + * + * @param message the message CharSequence to log. + * @since Log4j-2.6 + */ + public void error(final CharSequence message) { + logger.logIfEnabled(FQCN, ERROR, null, message, (Throwable) null); + } + + /** + * Logs a CharSequence at the {@code ERROR} level including the stack trace of + * the {@link Throwable} {@code t} passed as parameter. + * + * @param message the CharSequence to log. + * @param t the exception to log, including its stack trace. + * @since Log4j-2.6 + */ + public void error(final CharSequence message, final Throwable t) { + logger.logIfEnabled(FQCN, ERROR, null, message, t); + } + + /** + * Logs a message object with the {@code ERROR} level. + * + * @param message the message object to log. + */ + public void error(final String message) { + logger.logIfEnabled(FQCN, ERROR, null, message, (Throwable) null); + } + + /** + * Logs a message with parameters at the {@code ERROR} level. + * + * @param message the message to log; the format depends on the message factory. + * @param params parameters to the message. + * @see #getMessageFactory() + */ + public void error(final String message, final Object... params) { + logger.logIfEnabled(FQCN, ERROR, null, message, params); + } + + /** + * Logs a message with parameters at the {@code ERROR} level. + * + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void error(final String message, final Object p0) { + logger.logIfEnabled(FQCN, ERROR, null, message, p0); + } + + /** + * Logs a message with parameters at the {@code ERROR} level. + * + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void error(final String message, final Object p0, final Object p1) { + logger.logIfEnabled(FQCN, ERROR, null, message, p0, p1); + } + + /** + * Logs a message with parameters at the {@code ERROR} level. + * + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void error(final String message, final Object p0, final Object p1, final Object p2) { + logger.logIfEnabled(FQCN, ERROR, null, message, p0, p1, p2); + } + + /** + * Logs a message with parameters at the {@code ERROR} level. + * + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void error(final String message, final Object p0, final Object p1, final Object p2, + final Object p3) { + logger.logIfEnabled(FQCN, ERROR, null, message, p0, p1, p2, p3); + } + + /** + * Logs a message with parameters at the {@code ERROR} level. + * + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @param p4 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void error(final String message, final Object p0, final Object p1, final Object p2, + final Object p3, final Object p4) { + logger.logIfEnabled(FQCN, ERROR, null, message, p0, p1, p2, p3, p4); + } + + /** + * Logs a message with parameters at the {@code ERROR} level. + * + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @param p4 parameter to the message. + * @param p5 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void error(final String message, final Object p0, final Object p1, final Object p2, + final Object p3, final Object p4, final Object p5) { + logger.logIfEnabled(FQCN, ERROR, null, message, p0, p1, p2, p3, p4, p5); + } + + /** + * Logs a message with parameters at the {@code ERROR} level. + * + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @param p4 parameter to the message. + * @param p5 parameter to the message. + * @param p6 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void error(final String message, final Object p0, final Object p1, final Object p2, + final Object p3, final Object p4, final Object p5, final Object p6) { + logger.logIfEnabled(FQCN, ERROR, null, message, p0, p1, p2, p3, p4, p5, p6); + } + + /** + * Logs a message with parameters at the {@code ERROR} level. + * + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @param p4 parameter to the message. + * @param p5 parameter to the message. + * @param p6 parameter to the message. + * @param p7 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void error(final String message, final Object p0, final Object p1, final Object p2, + final Object p3, final Object p4, final Object p5, final Object p6, + final Object p7) { + logger.logIfEnabled(FQCN, ERROR, null, message, p0, p1, p2, p3, p4, p5, p6, p7); + } + + /** + * Logs a message with parameters at the {@code ERROR} level. + * + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @param p4 parameter to the message. + * @param p5 parameter to the message. + * @param p6 parameter to the message. + * @param p7 parameter to the message. + * @param p8 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void error(final String message, final Object p0, final Object p1, final Object p2, + final Object p3, final Object p4, final Object p5, final Object p6, + final Object p7, final Object p8) { + logger.logIfEnabled(FQCN, ERROR, null, message, p0, p1, p2, p3, p4, p5, p6, p7, p8); + } + + /** + * Logs a message with parameters at the {@code ERROR} level. + * + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @param p4 parameter to the message. + * @param p5 parameter to the message. + * @param p6 parameter to the message. + * @param p7 parameter to the message. + * @param p8 parameter to the message. + * @param p9 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void error(final String message, final Object p0, final Object p1, final Object p2, + final Object p3, final Object p4, final Object p5, final Object p6, + final Object p7, final Object p8, final Object p9) { + logger.logIfEnabled(FQCN, ERROR, null, message, p0, p1, p2, p3, p4, p5, p6, p7, p8, p9); + } + + /** + * Logs a message at the {@code ERROR} level including the stack trace of + * the {@link Throwable} {@code t} passed as parameter. + * + * @param message the message to log. + * @param t the exception to log, including its stack trace. + */ + public void error(final String message, final Throwable t) { + logger.logIfEnabled(FQCN, ERROR, null, message, t); + } + + /** + * Logs a message which is only to be constructed if the logging level is the {@code ERROR}level. + * + * @param msgSupplier A function, which when called, produces the desired log message; + * the format depends on the message factory. + * @since Log4j-2.4 + */ + public void error(final Supplier msgSupplier) { + logger.logIfEnabled(FQCN, ERROR, null, msgSupplier, (Throwable) null); + } + + /** + * Logs a message (only to be constructed if the logging level is the {@code ERROR} + * level) including the stack trace of the {@link Throwable} t passed as parameter. + * + * @param msgSupplier A function, which when called, produces the desired log message; + * the format depends on the message factory. + * @param t the exception to log, including its stack trace. + * @since Log4j-2.4 + */ + public void error(final Supplier msgSupplier, final Throwable t) { + logger.logIfEnabled(FQCN, ERROR, null, msgSupplier, t); + } + + /** + * Logs a message which is only to be constructed if the logging level is the + * {@code ERROR} level with the specified Marker. + * + * @param marker the marker data specific to this log statement + * @param msgSupplier A function, which when called, produces the desired log message; + * the format depends on the message factory. + * @since Log4j-2.4 + */ + public void error(final Marker marker, final Supplier msgSupplier) { + logger.logIfEnabled(FQCN, ERROR, marker, msgSupplier, (Throwable) null); + } + + /** + * Logs a message with parameters which are only to be constructed if the logging level is the + * {@code ERROR} level. + * + * @param marker the marker data specific to this log statement + * @param message the message to log; the format depends on the message factory. + * @param paramSuppliers An array of functions, which when called, produce the desired log message parameters. + * @since Log4j-2.4 + */ + public void error(final Marker marker, final String message, final Supplier... paramSuppliers) { + logger.logIfEnabled(FQCN, ERROR, marker, message, paramSuppliers); + } + + /** + * Logs a message (only to be constructed if the logging level is the {@code ERROR} + * level) with the specified Marker and including the stack trace of the {@link Throwable} + * t passed as parameter. + * + * @param marker the marker data specific to this log statement + * @param msgSupplier A function, which when called, produces the desired log message; + * the format depends on the message factory. + * @param t A Throwable or null. + * @since Log4j-2.4 + */ + public void error(final Marker marker, final Supplier msgSupplier, final Throwable t) { + logger.logIfEnabled(FQCN, ERROR, marker, msgSupplier, t); + } + + /** + * Logs a message with parameters which are only to be constructed if the logging level is + * the {@code ERROR} level. + * + * @param message the message to log; the format depends on the message factory. + * @param paramSuppliers An array of functions, which when called, produce the desired log message parameters. + * @since Log4j-2.4 + */ + public void error(final String message, final Supplier... paramSuppliers) { + logger.logIfEnabled(FQCN, ERROR, null, message, paramSuppliers); + } + + /** + * Logs a message which is only to be constructed if the logging level is the + * {@code ERROR} level with the specified Marker. The {@code MessageSupplier} may or may + * not use the {@link MessageFactory} to construct the {@code Message}. + * + * @param marker the marker data specific to this log statement + * @param msgSupplier A function, which when called, produces the desired log message. + * @since Log4j-2.4 + */ + public void error(final Marker marker, final MessageSupplier msgSupplier) { + logger.logIfEnabled(FQCN, ERROR, marker, msgSupplier, (Throwable) null); + } + + /** + * Logs a message (only to be constructed if the logging level is the {@code ERROR} + * level) with the specified Marker and including the stack trace of the {@link Throwable} + * t passed as parameter. The {@code MessageSupplier} may or may not use the + * {@link MessageFactory} to construct the {@code Message}. + * + * @param marker the marker data specific to this log statement + * @param msgSupplier A function, which when called, produces the desired log message. + * @param t A Throwable or null. + * @since Log4j-2.4 + */ + public void error(final Marker marker, final MessageSupplier msgSupplier, final Throwable t) { + logger.logIfEnabled(FQCN, ERROR, marker, msgSupplier, t); + } + + /** + * Logs a message which is only to be constructed if the logging level is the + * {@code ERROR} level. The {@code MessageSupplier} may or may not use the + * {@link MessageFactory} to construct the {@code Message}. + * + * @param msgSupplier A function, which when called, produces the desired log message. + * @since Log4j-2.4 + */ + public void error(final MessageSupplier msgSupplier) { + logger.logIfEnabled(FQCN, ERROR, null, msgSupplier, (Throwable) null); + } + + /** + * Logs a message (only to be constructed if the logging level is the {@code ERROR} + * level) including the stack trace of the {@link Throwable} t passed as parameter. + * The {@code MessageSupplier} may or may not use the {@link MessageFactory} to construct the + * {@code Message}. + * + * @param msgSupplier A function, which when called, produces the desired log message. + * @param t the exception to log, including its stack trace. + * @since Log4j-2.4 + */ + public void error(final MessageSupplier msgSupplier, final Throwable t) { + logger.logIfEnabled(FQCN, ERROR, null, msgSupplier, t); + } + + /** + * Logs a message with the specific Marker at the {@code DEBUG} level. + * + * @param marker the marker data specific to this log statement + * @param msg the message string to be logged + */ + public void debug(final Marker marker, final Message msg) { + logger.logIfEnabled(FQCN, DEBUG, marker, msg, (Throwable) null); + } + + /** + * Logs a message with the specific Marker at the {@code DEBUG} level. + * + * @param marker the marker data specific to this log statement + * @param msg the message string to be logged + * @param t A Throwable or null. + */ + public void debug(final Marker marker, final Message msg, final Throwable t) { + logger.logIfEnabled(FQCN, DEBUG, marker, msg, t); + } + + /** + * Logs a message object with the {@code DEBUG} level. + * + * @param marker the marker data specific to this log statement + * @param message the message object to log. + */ + public void debug(final Marker marker, final Object message) { + logger.logIfEnabled(FQCN, DEBUG, marker, message, (Throwable) null); + } + + /** + * Logs a message CharSequence with the {@code DEBUG} level. + * + * @param marker the marker data specific to this log statement + * @param message the message CharSequence to log. + * @since Log4j-2.6 + */ + public void debug(final Marker marker, final CharSequence message) { + logger.logIfEnabled(FQCN, DEBUG, marker, message, (Throwable) null); + } + + /** + * Logs a message at the {@code DEBUG} level including the stack trace of + * the {@link Throwable} {@code t} passed as parameter. + * + * @param marker the marker data specific to this log statement + * @param message the message to log. + * @param t the exception to log, including its stack trace. + */ + public void debug(final Marker marker, final Object message, final Throwable t) { + logger.logIfEnabled(FQCN, DEBUG, marker, message, t); + } + + /** + * Logs a message at the {@code DEBUG} level including the stack trace of + * the {@link Throwable} {@code t} passed as parameter. + * + * @param marker the marker data specific to this log statement + * @param message the CharSequence to log. + * @param t the exception to log, including its stack trace. + * @since Log4j-2.6 + */ + public void debug(final Marker marker, final CharSequence message, final Throwable t) { + logger.logIfEnabled(FQCN, DEBUG, marker, message, t); + } + + /** + * Logs a message object with the {@code DEBUG} level. + * + * @param marker the marker data specific to this log statement + * @param message the message object to log. + */ + public void debug(final Marker marker, final String message) { + logger.logIfEnabled(FQCN, DEBUG, marker, message, (Throwable) null); + } + + /** + * Logs a message with parameters at the {@code DEBUG} level. + * + * @param marker the marker data specific to this log statement + * @param message the message to log; the format depends on the message factory. + * @param params parameters to the message. + * @see #getMessageFactory() + */ + public void debug(final Marker marker, final String message, final Object... params) { + logger.logIfEnabled(FQCN, DEBUG, marker, message, params); + } + + /** + * Logs a message with parameters at the {@code DEBUG} level. + * + * @param marker the marker data specific to this log statement + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void debug(final Marker marker, final String message, final Object p0) { + logger.logIfEnabled(FQCN, DEBUG, marker, message, p0); + } + + /** + * Logs a message with parameters at the {@code DEBUG} level. + * + * @param marker the marker data specific to this log statement + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void debug(final Marker marker, final String message, final Object p0, final Object p1) { + logger.logIfEnabled(FQCN, DEBUG, marker, message, p0, p1); + } + + /** + * Logs a message with parameters at the {@code DEBUG} level. + * + * @param marker the marker data specific to this log statement + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void debug(final Marker marker, final String message, final Object p0, final Object p1, final Object p2) { + logger.logIfEnabled(FQCN, DEBUG, marker, message, p0, p1, p2); + } + + /** + * Logs a message with parameters at the {@code DEBUG} level. + * + * @param marker the marker data specific to this log statement + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void debug(final Marker marker, final String message, final Object p0, final Object p1, final Object p2, + final Object p3) { + logger.logIfEnabled(FQCN, DEBUG, marker, message, p0, p1, p2, p3); + } + + /** + * Logs a message with parameters at the {@code DEBUG} level. + * + * @param marker the marker data specific to this log statement + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @param p4 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void debug(final Marker marker, final String message, final Object p0, final Object p1, final Object p2, + final Object p3, final Object p4) { + logger.logIfEnabled(FQCN, DEBUG, marker, message, p0, p1, p2, p3, p4); + } + + /** + * Logs a message with parameters at the {@code DEBUG} level. + * + * @param marker the marker data specific to this log statement + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @param p4 parameter to the message. + * @param p5 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void debug(final Marker marker, final String message, final Object p0, final Object p1, final Object p2, + final Object p3, final Object p4, final Object p5) { + logger.logIfEnabled(FQCN, DEBUG, marker, message, p0, p1, p2, p3, p4, p5); + } + + /** + * Logs a message with parameters at the {@code DEBUG} level. + * + * @param marker the marker data specific to this log statement + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @param p4 parameter to the message. + * @param p5 parameter to the message. + * @param p6 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void debug(final Marker marker, final String message, final Object p0, final Object p1, final Object p2, + final Object p3, final Object p4, final Object p5, final Object p6) { + logger.logIfEnabled(FQCN, DEBUG, marker, message, p0, p1, p2, p3, p4, p5, p6); + } + + /** + * Logs a message with parameters at the {@code DEBUG} level. + * + * @param marker the marker data specific to this log statement + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @param p4 parameter to the message. + * @param p5 parameter to the message. + * @param p6 parameter to the message. + * @param p7 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void debug(final Marker marker, final String message, final Object p0, final Object p1, final Object p2, + final Object p3, final Object p4, final Object p5, final Object p6, + final Object p7) { + logger.logIfEnabled(FQCN, DEBUG, marker, message, p0, p1, p2, p3, p4, p5, p6, p7); + } + + /** + * Logs a message with parameters at the {@code DEBUG} level. + * + * @param marker the marker data specific to this log statement + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @param p4 parameter to the message. + * @param p5 parameter to the message. + * @param p6 parameter to the message. + * @param p7 parameter to the message. + * @param p8 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void debug(final Marker marker, final String message, final Object p0, final Object p1, final Object p2, + final Object p3, final Object p4, final Object p5, final Object p6, + final Object p7, final Object p8) { + logger.logIfEnabled(FQCN, DEBUG, marker, message, p0, p1, p2, p3, p4, p5, p6, p7, p8); + } + + /** + * Logs a message with parameters at the {@code DEBUG} level. + * + * @param marker the marker data specific to this log statement + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @param p4 parameter to the message. + * @param p5 parameter to the message. + * @param p6 parameter to the message. + * @param p7 parameter to the message. + * @param p8 parameter to the message. + * @param p9 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void debug(final Marker marker, final String message, final Object p0, final Object p1, final Object p2, + final Object p3, final Object p4, final Object p5, final Object p6, + final Object p7, final Object p8, final Object p9) { + logger.logIfEnabled(FQCN, DEBUG, marker, message, p0, p1, p2, p3, p4, p5, p6, p7, p8, p9); + } + + /** + * Logs a message at the {@code DEBUG} level including the stack trace of + * the {@link Throwable} {@code t} passed as parameter. + * + * @param marker the marker data specific to this log statement + * @param message the message to log. + * @param t the exception to log, including its stack trace. + */ + public void debug(final Marker marker, final String message, final Throwable t) { + logger.logIfEnabled(FQCN, DEBUG, marker, message, t); + } + + /** + * Logs the specified Message at the {@code DEBUG} level. + * + * @param msg the message string to be logged + */ + public void debug(final Message msg) { + logger.logIfEnabled(FQCN, DEBUG, null, msg, (Throwable) null); + } + + /** + * Logs the specified Message at the {@code DEBUG} level. + * + * @param msg the message string to be logged + * @param t A Throwable or null. + */ + public void debug(final Message msg, final Throwable t) { + logger.logIfEnabled(FQCN, DEBUG, null, msg, t); + } + + /** + * Logs a message object with the {@code DEBUG} level. + * + * @param message the message object to log. + */ + public void debug(final Object message) { + logger.logIfEnabled(FQCN, DEBUG, null, message, (Throwable) null); + } + + /** + * Logs a message at the {@code DEBUG} level including the stack trace of + * the {@link Throwable} {@code t} passed as parameter. + * + * @param message the message to log. + * @param t the exception to log, including its stack trace. + */ + public void debug(final Object message, final Throwable t) { + logger.logIfEnabled(FQCN, DEBUG, null, message, t); + } + + /** + * Logs a message CharSequence with the {@code DEBUG} level. + * + * @param message the message CharSequence to log. + * @since Log4j-2.6 + */ + public void debug(final CharSequence message) { + logger.logIfEnabled(FQCN, DEBUG, null, message, (Throwable) null); + } + + /** + * Logs a CharSequence at the {@code DEBUG} level including the stack trace of + * the {@link Throwable} {@code t} passed as parameter. + * + * @param message the CharSequence to log. + * @param t the exception to log, including its stack trace. + * @since Log4j-2.6 + */ + public void debug(final CharSequence message, final Throwable t) { + logger.logIfEnabled(FQCN, DEBUG, null, message, t); + } + + /** + * Logs a message object with the {@code DEBUG} level. + * + * @param message the message object to log. + */ + public void debug(final String message) { + logger.logIfEnabled(FQCN, DEBUG, null, message, (Throwable) null); + } + + /** + * Logs a message with parameters at the {@code DEBUG} level. + * + * @param message the message to log; the format depends on the message factory. + * @param params parameters to the message. + * @see #getMessageFactory() + */ + public void debug(final String message, final Object... params) { + logger.logIfEnabled(FQCN, DEBUG, null, message, params); + } + + /** + * Logs a message with parameters at the {@code DEBUG} level. + * + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void debug(final String message, final Object p0) { + logger.logIfEnabled(FQCN, DEBUG, null, message, p0); + } + + /** + * Logs a message with parameters at the {@code DEBUG} level. + * + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void debug(final String message, final Object p0, final Object p1) { + logger.logIfEnabled(FQCN, DEBUG, null, message, p0, p1); + } + + /** + * Logs a message with parameters at the {@code DEBUG} level. + * + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void debug(final String message, final Object p0, final Object p1, final Object p2) { + logger.logIfEnabled(FQCN, DEBUG, null, message, p0, p1, p2); + } + + /** + * Logs a message with parameters at the {@code DEBUG} level. + * + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void debug(final String message, final Object p0, final Object p1, final Object p2, + final Object p3) { + logger.logIfEnabled(FQCN, DEBUG, null, message, p0, p1, p2, p3); + } + + /** + * Logs a message with parameters at the {@code DEBUG} level. + * + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @param p4 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void debug(final String message, final Object p0, final Object p1, final Object p2, + final Object p3, final Object p4) { + logger.logIfEnabled(FQCN, DEBUG, null, message, p0, p1, p2, p3, p4); + } + + /** + * Logs a message with parameters at the {@code DEBUG} level. + * + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @param p4 parameter to the message. + * @param p5 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void debug(final String message, final Object p0, final Object p1, final Object p2, + final Object p3, final Object p4, final Object p5) { + logger.logIfEnabled(FQCN, DEBUG, null, message, p0, p1, p2, p3, p4, p5); + } + + /** + * Logs a message with parameters at the {@code DEBUG} level. + * + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @param p4 parameter to the message. + * @param p5 parameter to the message. + * @param p6 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void debug(final String message, final Object p0, final Object p1, final Object p2, + final Object p3, final Object p4, final Object p5, final Object p6) { + logger.logIfEnabled(FQCN, DEBUG, null, message, p0, p1, p2, p3, p4, p5, p6); + } + + /** + * Logs a message with parameters at the {@code DEBUG} level. + * + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @param p4 parameter to the message. + * @param p5 parameter to the message. + * @param p6 parameter to the message. + * @param p7 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void debug(final String message, final Object p0, final Object p1, final Object p2, + final Object p3, final Object p4, final Object p5, final Object p6, + final Object p7) { + logger.logIfEnabled(FQCN, DEBUG, null, message, p0, p1, p2, p3, p4, p5, p6, p7); + } + + /** + * Logs a message with parameters at the {@code DEBUG} level. + * + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @param p4 parameter to the message. + * @param p5 parameter to the message. + * @param p6 parameter to the message. + * @param p7 parameter to the message. + * @param p8 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void debug(final String message, final Object p0, final Object p1, final Object p2, + final Object p3, final Object p4, final Object p5, final Object p6, + final Object p7, final Object p8) { + logger.logIfEnabled(FQCN, DEBUG, null, message, p0, p1, p2, p3, p4, p5, p6, p7, p8); + } + + /** + * Logs a message with parameters at the {@code DEBUG} level. + * + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @param p4 parameter to the message. + * @param p5 parameter to the message. + * @param p6 parameter to the message. + * @param p7 parameter to the message. + * @param p8 parameter to the message. + * @param p9 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void debug(final String message, final Object p0, final Object p1, final Object p2, + final Object p3, final Object p4, final Object p5, final Object p6, + final Object p7, final Object p8, final Object p9) { + logger.logIfEnabled(FQCN, DEBUG, null, message, p0, p1, p2, p3, p4, p5, p6, p7, p8, p9); + } + + /** + * Logs a message at the {@code DEBUG} level including the stack trace of + * the {@link Throwable} {@code t} passed as parameter. + * + * @param message the message to log. + * @param t the exception to log, including its stack trace. + */ + public void debug(final String message, final Throwable t) { + logger.logIfEnabled(FQCN, DEBUG, null, message, t); + } + + /** + * Logs a message which is only to be constructed if the logging level is the {@code DEBUG}level. + * + * @param msgSupplier A function, which when called, produces the desired log message; + * the format depends on the message factory. + * @since Log4j-2.4 + */ + public void debug(final Supplier msgSupplier) { + logger.logIfEnabled(FQCN, DEBUG, null, msgSupplier, (Throwable) null); + } + + /** + * Logs a message (only to be constructed if the logging level is the {@code DEBUG} + * level) including the stack trace of the {@link Throwable} t passed as parameter. + * + * @param msgSupplier A function, which when called, produces the desired log message; + * the format depends on the message factory. + * @param t the exception to log, including its stack trace. + * @since Log4j-2.4 + */ + public void debug(final Supplier msgSupplier, final Throwable t) { + logger.logIfEnabled(FQCN, DEBUG, null, msgSupplier, t); + } + + /** + * Logs a message which is only to be constructed if the logging level is the + * {@code DEBUG} level with the specified Marker. + * + * @param marker the marker data specific to this log statement + * @param msgSupplier A function, which when called, produces the desired log message; + * the format depends on the message factory. + * @since Log4j-2.4 + */ + public void debug(final Marker marker, final Supplier msgSupplier) { + logger.logIfEnabled(FQCN, DEBUG, marker, msgSupplier, (Throwable) null); + } + + /** + * Logs a message with parameters which are only to be constructed if the logging level is the + * {@code DEBUG} level. + * + * @param marker the marker data specific to this log statement + * @param message the message to log; the format depends on the message factory. + * @param paramSuppliers An array of functions, which when called, produce the desired log message parameters. + * @since Log4j-2.4 + */ + public void debug(final Marker marker, final String message, final Supplier... paramSuppliers) { + logger.logIfEnabled(FQCN, DEBUG, marker, message, paramSuppliers); + } + + /** + * Logs a message (only to be constructed if the logging level is the {@code DEBUG} + * level) with the specified Marker and including the stack trace of the {@link Throwable} + * t passed as parameter. + * + * @param marker the marker data specific to this log statement + * @param msgSupplier A function, which when called, produces the desired log message; + * the format depends on the message factory. + * @param t A Throwable or null. + * @since Log4j-2.4 + */ + public void debug(final Marker marker, final Supplier msgSupplier, final Throwable t) { + logger.logIfEnabled(FQCN, DEBUG, marker, msgSupplier, t); + } + + /** + * Logs a message with parameters which are only to be constructed if the logging level is + * the {@code DEBUG} level. + * + * @param message the message to log; the format depends on the message factory. + * @param paramSuppliers An array of functions, which when called, produce the desired log message parameters. + * @since Log4j-2.4 + */ + public void debug(final String message, final Supplier... paramSuppliers) { + logger.logIfEnabled(FQCN, DEBUG, null, message, paramSuppliers); + } + + /** + * Logs a message which is only to be constructed if the logging level is the + * {@code DEBUG} level with the specified Marker. The {@code MessageSupplier} may or may + * not use the {@link MessageFactory} to construct the {@code Message}. + * + * @param marker the marker data specific to this log statement + * @param msgSupplier A function, which when called, produces the desired log message. + * @since Log4j-2.4 + */ + public void debug(final Marker marker, final MessageSupplier msgSupplier) { + logger.logIfEnabled(FQCN, DEBUG, marker, msgSupplier, (Throwable) null); + } + + /** + * Logs a message (only to be constructed if the logging level is the {@code DEBUG} + * level) with the specified Marker and including the stack trace of the {@link Throwable} + * t passed as parameter. The {@code MessageSupplier} may or may not use the + * {@link MessageFactory} to construct the {@code Message}. + * + * @param marker the marker data specific to this log statement + * @param msgSupplier A function, which when called, produces the desired log message. + * @param t A Throwable or null. + * @since Log4j-2.4 + */ + public void debug(final Marker marker, final MessageSupplier msgSupplier, final Throwable t) { + logger.logIfEnabled(FQCN, DEBUG, marker, msgSupplier, t); + } + + /** + * Logs a message which is only to be constructed if the logging level is the + * {@code DEBUG} level. The {@code MessageSupplier} may or may not use the + * {@link MessageFactory} to construct the {@code Message}. + * + * @param msgSupplier A function, which when called, produces the desired log message. + * @since Log4j-2.4 + */ + public void debug(final MessageSupplier msgSupplier) { + logger.logIfEnabled(FQCN, DEBUG, null, msgSupplier, (Throwable) null); + } + + /** + * Logs a message (only to be constructed if the logging level is the {@code DEBUG} + * level) including the stack trace of the {@link Throwable} t passed as parameter. + * The {@code MessageSupplier} may or may not use the {@link MessageFactory} to construct the + * {@code Message}. + * + * @param msgSupplier A function, which when called, produces the desired log message. + * @param t the exception to log, including its stack trace. + * @since Log4j-2.4 + */ + public void debug(final MessageSupplier msgSupplier, final Throwable t) { + logger.logIfEnabled(FQCN, DEBUG, null, msgSupplier, t); + } + + /** + * Logs a message with the specific Marker at the {@code ECHO} level. + * + * @param marker the marker data specific to this log statement + * @param msg the message string to be logged + */ + public void echo(final Marker marker, final Message msg) { + logger.logIfEnabled(FQCN, ECHO, marker, msg, (Throwable) null); + } + + /** + * Logs a message with the specific Marker at the {@code ECHO} level. + * + * @param marker the marker data specific to this log statement + * @param msg the message string to be logged + * @param t A Throwable or null. + */ + public void echo(final Marker marker, final Message msg, final Throwable t) { + logger.logIfEnabled(FQCN, ECHO, marker, msg, t); + } + + /** + * Logs a message object with the {@code ECHO} level. + * + * @param marker the marker data specific to this log statement + * @param message the message object to log. + */ + public void echo(final Marker marker, final Object message) { + logger.logIfEnabled(FQCN, ECHO, marker, message, (Throwable) null); + } + + /** + * Logs a message CharSequence with the {@code ECHO} level. + * + * @param marker the marker data specific to this log statement + * @param message the message CharSequence to log. + * @since Log4j-2.6 + */ + public void echo(final Marker marker, final CharSequence message) { + logger.logIfEnabled(FQCN, ECHO, marker, message, (Throwable) null); + } + + /** + * Logs a message at the {@code ECHO} level including the stack trace of + * the {@link Throwable} {@code t} passed as parameter. + * + * @param marker the marker data specific to this log statement + * @param message the message to log. + * @param t the exception to log, including its stack trace. + */ + public void echo(final Marker marker, final Object message, final Throwable t) { + logger.logIfEnabled(FQCN, ECHO, marker, message, t); + } + + /** + * Logs a message at the {@code ECHO} level including the stack trace of + * the {@link Throwable} {@code t} passed as parameter. + * + * @param marker the marker data specific to this log statement + * @param message the CharSequence to log. + * @param t the exception to log, including its stack trace. + * @since Log4j-2.6 + */ + public void echo(final Marker marker, final CharSequence message, final Throwable t) { + logger.logIfEnabled(FQCN, ECHO, marker, message, t); + } + + /** + * Logs a message object with the {@code ECHO} level. + * + * @param marker the marker data specific to this log statement + * @param message the message object to log. + */ + public void echo(final Marker marker, final String message) { + logger.logIfEnabled(FQCN, ECHO, marker, message, (Throwable) null); + } + + /** + * Logs a message with parameters at the {@code ECHO} level. + * + * @param marker the marker data specific to this log statement + * @param message the message to log; the format depends on the message factory. + * @param params parameters to the message. + * @see #getMessageFactory() + */ + public void echo(final Marker marker, final String message, final Object... params) { + logger.logIfEnabled(FQCN, ECHO, marker, message, params); + } + + /** + * Logs a message with parameters at the {@code ECHO} level. + * + * @param marker the marker data specific to this log statement + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void echo(final Marker marker, final String message, final Object p0) { + logger.logIfEnabled(FQCN, ECHO, marker, message, p0); + } + + /** + * Logs a message with parameters at the {@code ECHO} level. + * + * @param marker the marker data specific to this log statement + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void echo(final Marker marker, final String message, final Object p0, final Object p1) { + logger.logIfEnabled(FQCN, ECHO, marker, message, p0, p1); + } + + /** + * Logs a message with parameters at the {@code ECHO} level. + * + * @param marker the marker data specific to this log statement + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void echo(final Marker marker, final String message, final Object p0, final Object p1, final Object p2) { + logger.logIfEnabled(FQCN, ECHO, marker, message, p0, p1, p2); + } + + /** + * Logs a message with parameters at the {@code ECHO} level. + * + * @param marker the marker data specific to this log statement + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void echo(final Marker marker, final String message, final Object p0, final Object p1, final Object p2, + final Object p3) { + logger.logIfEnabled(FQCN, ECHO, marker, message, p0, p1, p2, p3); + } + + /** + * Logs a message with parameters at the {@code ECHO} level. + * + * @param marker the marker data specific to this log statement + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @param p4 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void echo(final Marker marker, final String message, final Object p0, final Object p1, final Object p2, + final Object p3, final Object p4) { + logger.logIfEnabled(FQCN, ECHO, marker, message, p0, p1, p2, p3, p4); + } + + /** + * Logs a message with parameters at the {@code ECHO} level. + * + * @param marker the marker data specific to this log statement + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @param p4 parameter to the message. + * @param p5 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void echo(final Marker marker, final String message, final Object p0, final Object p1, final Object p2, + final Object p3, final Object p4, final Object p5) { + logger.logIfEnabled(FQCN, ECHO, marker, message, p0, p1, p2, p3, p4, p5); + } + + /** + * Logs a message with parameters at the {@code ECHO} level. + * + * @param marker the marker data specific to this log statement + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @param p4 parameter to the message. + * @param p5 parameter to the message. + * @param p6 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void echo(final Marker marker, final String message, final Object p0, final Object p1, final Object p2, + final Object p3, final Object p4, final Object p5, final Object p6) { + logger.logIfEnabled(FQCN, ECHO, marker, message, p0, p1, p2, p3, p4, p5, p6); + } + + /** + * Logs a message with parameters at the {@code ECHO} level. + * + * @param marker the marker data specific to this log statement + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @param p4 parameter to the message. + * @param p5 parameter to the message. + * @param p6 parameter to the message. + * @param p7 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void echo(final Marker marker, final String message, final Object p0, final Object p1, final Object p2, + final Object p3, final Object p4, final Object p5, final Object p6, + final Object p7) { + logger.logIfEnabled(FQCN, ECHO, marker, message, p0, p1, p2, p3, p4, p5, p6, p7); + } + + /** + * Logs a message with parameters at the {@code ECHO} level. + * + * @param marker the marker data specific to this log statement + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @param p4 parameter to the message. + * @param p5 parameter to the message. + * @param p6 parameter to the message. + * @param p7 parameter to the message. + * @param p8 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void echo(final Marker marker, final String message, final Object p0, final Object p1, final Object p2, + final Object p3, final Object p4, final Object p5, final Object p6, + final Object p7, final Object p8) { + logger.logIfEnabled(FQCN, ECHO, marker, message, p0, p1, p2, p3, p4, p5, p6, p7, p8); + } + + /** + * Logs a message with parameters at the {@code ECHO} level. + * + * @param marker the marker data specific to this log statement + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @param p4 parameter to the message. + * @param p5 parameter to the message. + * @param p6 parameter to the message. + * @param p7 parameter to the message. + * @param p8 parameter to the message. + * @param p9 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void echo(final Marker marker, final String message, final Object p0, final Object p1, final Object p2, + final Object p3, final Object p4, final Object p5, final Object p6, + final Object p7, final Object p8, final Object p9) { + logger.logIfEnabled(FQCN, ECHO, marker, message, p0, p1, p2, p3, p4, p5, p6, p7, p8, p9); + } + + /** + * Logs a message at the {@code ECHO} level including the stack trace of + * the {@link Throwable} {@code t} passed as parameter. + * + * @param marker the marker data specific to this log statement + * @param message the message to log. + * @param t the exception to log, including its stack trace. + */ + public void echo(final Marker marker, final String message, final Throwable t) { + logger.logIfEnabled(FQCN, ECHO, marker, message, t); + } + + /** + * Logs the specified Message at the {@code ECHO} level. + * + * @param msg the message string to be logged + */ + public void echo(final Message msg) { + logger.logIfEnabled(FQCN, ECHO, null, msg, (Throwable) null); + } + + /** + * Logs the specified Message at the {@code ECHO} level. + * + * @param msg the message string to be logged + * @param t A Throwable or null. + */ + public void echo(final Message msg, final Throwable t) { + logger.logIfEnabled(FQCN, ECHO, null, msg, t); + } + + /** + * Logs a message object with the {@code ECHO} level. + * + * @param message the message object to log. + */ + public void echo(final Object message) { + logger.logIfEnabled(FQCN, ECHO, null, message, (Throwable) null); + } + + /** + * Logs a message at the {@code ECHO} level including the stack trace of + * the {@link Throwable} {@code t} passed as parameter. + * + * @param message the message to log. + * @param t the exception to log, including its stack trace. + */ + public void echo(final Object message, final Throwable t) { + logger.logIfEnabled(FQCN, ECHO, null, message, t); + } + + /** + * Logs a message CharSequence with the {@code ECHO} level. + * + * @param message the message CharSequence to log. + * @since Log4j-2.6 + */ + public void echo(final CharSequence message) { + logger.logIfEnabled(FQCN, ECHO, null, message, (Throwable) null); + } + + /** + * Logs a CharSequence at the {@code ECHO} level including the stack trace of + * the {@link Throwable} {@code t} passed as parameter. + * + * @param message the CharSequence to log. + * @param t the exception to log, including its stack trace. + * @since Log4j-2.6 + */ + public void echo(final CharSequence message, final Throwable t) { + logger.logIfEnabled(FQCN, ECHO, null, message, t); + } + + /** + * Logs a message object with the {@code ECHO} level. + * + * @param message the message object to log. + */ + public void echo(final String message) { + logger.logIfEnabled(FQCN, ECHO, null, message, (Throwable) null); + } + + /** + * Logs a message with parameters at the {@code ECHO} level. + * + * @param message the message to log; the format depends on the message factory. + * @param params parameters to the message. + * @see #getMessageFactory() + */ + public void echo(final String message, final Object... params) { + logger.logIfEnabled(FQCN, ECHO, null, message, params); + } + + /** + * Logs a message with parameters at the {@code ECHO} level. + * + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void echo(final String message, final Object p0) { + logger.logIfEnabled(FQCN, ECHO, null, message, p0); + } + + /** + * Logs a message with parameters at the {@code ECHO} level. + * + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void echo(final String message, final Object p0, final Object p1) { + logger.logIfEnabled(FQCN, ECHO, null, message, p0, p1); + } + + /** + * Logs a message with parameters at the {@code ECHO} level. + * + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void echo(final String message, final Object p0, final Object p1, final Object p2) { + logger.logIfEnabled(FQCN, ECHO, null, message, p0, p1, p2); + } + + /** + * Logs a message with parameters at the {@code ECHO} level. + * + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void echo(final String message, final Object p0, final Object p1, final Object p2, + final Object p3) { + logger.logIfEnabled(FQCN, ECHO, null, message, p0, p1, p2, p3); + } + + /** + * Logs a message with parameters at the {@code ECHO} level. + * + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @param p4 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void echo(final String message, final Object p0, final Object p1, final Object p2, + final Object p3, final Object p4) { + logger.logIfEnabled(FQCN, ECHO, null, message, p0, p1, p2, p3, p4); + } + + /** + * Logs a message with parameters at the {@code ECHO} level. + * + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @param p4 parameter to the message. + * @param p5 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void echo(final String message, final Object p0, final Object p1, final Object p2, + final Object p3, final Object p4, final Object p5) { + logger.logIfEnabled(FQCN, ECHO, null, message, p0, p1, p2, p3, p4, p5); + } + + /** + * Logs a message with parameters at the {@code ECHO} level. + * + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @param p4 parameter to the message. + * @param p5 parameter to the message. + * @param p6 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void echo(final String message, final Object p0, final Object p1, final Object p2, + final Object p3, final Object p4, final Object p5, final Object p6) { + logger.logIfEnabled(FQCN, ECHO, null, message, p0, p1, p2, p3, p4, p5, p6); + } + + /** + * Logs a message with parameters at the {@code ECHO} level. + * + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @param p4 parameter to the message. + * @param p5 parameter to the message. + * @param p6 parameter to the message. + * @param p7 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void echo(final String message, final Object p0, final Object p1, final Object p2, + final Object p3, final Object p4, final Object p5, final Object p6, + final Object p7) { + logger.logIfEnabled(FQCN, ECHO, null, message, p0, p1, p2, p3, p4, p5, p6, p7); + } + + /** + * Logs a message with parameters at the {@code ECHO} level. + * + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @param p4 parameter to the message. + * @param p5 parameter to the message. + * @param p6 parameter to the message. + * @param p7 parameter to the message. + * @param p8 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void echo(final String message, final Object p0, final Object p1, final Object p2, + final Object p3, final Object p4, final Object p5, final Object p6, + final Object p7, final Object p8) { + logger.logIfEnabled(FQCN, ECHO, null, message, p0, p1, p2, p3, p4, p5, p6, p7, p8); + } + + /** + * Logs a message with parameters at the {@code ECHO} level. + * + * @param message the message to log; the format depends on the message factory. + * @param p0 parameter to the message. + * @param p1 parameter to the message. + * @param p2 parameter to the message. + * @param p3 parameter to the message. + * @param p4 parameter to the message. + * @param p5 parameter to the message. + * @param p6 parameter to the message. + * @param p7 parameter to the message. + * @param p8 parameter to the message. + * @param p9 parameter to the message. + * @see #getMessageFactory() + * @since Log4j-2.6 + */ + public void echo(final String message, final Object p0, final Object p1, final Object p2, + final Object p3, final Object p4, final Object p5, final Object p6, + final Object p7, final Object p8, final Object p9) { + logger.logIfEnabled(FQCN, ECHO, null, message, p0, p1, p2, p3, p4, p5, p6, p7, p8, p9); + } + + /** + * Logs a message at the {@code ECHO} level including the stack trace of + * the {@link Throwable} {@code t} passed as parameter. + * + * @param message the message to log. + * @param t the exception to log, including its stack trace. + */ + public void echo(final String message, final Throwable t) { + logger.logIfEnabled(FQCN, ECHO, null, message, t); + } + + /** + * Logs a message which is only to be constructed if the logging level is the {@code ECHO}level. + * + * @param msgSupplier A function, which when called, produces the desired log message; + * the format depends on the message factory. + * @since Log4j-2.4 + */ + public void echo(final Supplier msgSupplier) { + logger.logIfEnabled(FQCN, ECHO, null, msgSupplier, (Throwable) null); + } + + /** + * Logs a message (only to be constructed if the logging level is the {@code ECHO} + * level) including the stack trace of the {@link Throwable} t passed as parameter. + * + * @param msgSupplier A function, which when called, produces the desired log message; + * the format depends on the message factory. + * @param t the exception to log, including its stack trace. + * @since Log4j-2.4 + */ + public void echo(final Supplier msgSupplier, final Throwable t) { + logger.logIfEnabled(FQCN, ECHO, null, msgSupplier, t); + } + + /** + * Logs a message which is only to be constructed if the logging level is the + * {@code ECHO} level with the specified Marker. + * + * @param marker the marker data specific to this log statement + * @param msgSupplier A function, which when called, produces the desired log message; + * the format depends on the message factory. + * @since Log4j-2.4 + */ + public void echo(final Marker marker, final Supplier msgSupplier) { + logger.logIfEnabled(FQCN, ECHO, marker, msgSupplier, (Throwable) null); + } + + /** + * Logs a message with parameters which are only to be constructed if the logging level is the + * {@code ECHO} level. + * + * @param marker the marker data specific to this log statement + * @param message the message to log; the format depends on the message factory. + * @param paramSuppliers An array of functions, which when called, produce the desired log message parameters. + * @since Log4j-2.4 + */ + public void echo(final Marker marker, final String message, final Supplier... paramSuppliers) { + logger.logIfEnabled(FQCN, ECHO, marker, message, paramSuppliers); + } + + /** + * Logs a message (only to be constructed if the logging level is the {@code ECHO} + * level) with the specified Marker and including the stack trace of the {@link Throwable} + * t passed as parameter. + * + * @param marker the marker data specific to this log statement + * @param msgSupplier A function, which when called, produces the desired log message; + * the format depends on the message factory. + * @param t A Throwable or null. + * @since Log4j-2.4 + */ + public void echo(final Marker marker, final Supplier msgSupplier, final Throwable t) { + logger.logIfEnabled(FQCN, ECHO, marker, msgSupplier, t); + } + + /** + * Logs a message with parameters which are only to be constructed if the logging level is + * the {@code ECHO} level. + * + * @param message the message to log; the format depends on the message factory. + * @param paramSuppliers An array of functions, which when called, produce the desired log message parameters. + * @since Log4j-2.4 + */ + public void echo(final String message, final Supplier... paramSuppliers) { + logger.logIfEnabled(FQCN, ECHO, null, message, paramSuppliers); + } + + /** + * Logs a message which is only to be constructed if the logging level is the + * {@code ECHO} level with the specified Marker. The {@code MessageSupplier} may or may + * not use the {@link MessageFactory} to construct the {@code Message}. + * + * @param marker the marker data specific to this log statement + * @param msgSupplier A function, which when called, produces the desired log message. + * @since Log4j-2.4 + */ + public void echo(final Marker marker, final MessageSupplier msgSupplier) { + logger.logIfEnabled(FQCN, ECHO, marker, msgSupplier, (Throwable) null); + } + + /** + * Logs a message (only to be constructed if the logging level is the {@code ECHO} + * level) with the specified Marker and including the stack trace of the {@link Throwable} + * t passed as parameter. The {@code MessageSupplier} may or may not use the + * {@link MessageFactory} to construct the {@code Message}. + * + * @param marker the marker data specific to this log statement + * @param msgSupplier A function, which when called, produces the desired log message. + * @param t A Throwable or null. + * @since Log4j-2.4 + */ + public void echo(final Marker marker, final MessageSupplier msgSupplier, final Throwable t) { + logger.logIfEnabled(FQCN, ECHO, marker, msgSupplier, t); + } + + /** + * Logs a message which is only to be constructed if the logging level is the + * {@code ECHO} level. The {@code MessageSupplier} may or may not use the + * {@link MessageFactory} to construct the {@code Message}. + * + * @param msgSupplier A function, which when called, produces the desired log message. + * @since Log4j-2.4 + */ + public void echo(final MessageSupplier msgSupplier) { + logger.logIfEnabled(FQCN, ECHO, null, msgSupplier, (Throwable) null); + } + + /** + * Logs a message (only to be constructed if the logging level is the {@code ECHO} + * level) including the stack trace of the {@link Throwable} t passed as parameter. + * The {@code MessageSupplier} may or may not use the {@link MessageFactory} to construct the + * {@code Message}. + * + * @param msgSupplier A function, which when called, produces the desired log message. + * @param t the exception to log, including its stack trace. + * @since Log4j-2.4 + */ + public void echo(final MessageSupplier msgSupplier, final Throwable t) { + logger.logIfEnabled(FQCN, ECHO, null, msgSupplier, t); + } +} + diff --git a/src/main/java/org/qortal/at/QortalAtLoggerFactory.java b/src/main/java/org/qortal/at/QortalAtLoggerFactory.java new file mode 100644 index 00000000..19cbb3d9 --- /dev/null +++ b/src/main/java/org/qortal/at/QortalAtLoggerFactory.java @@ -0,0 +1,24 @@ +package org.qortal.at; + +import org.ciyam.at.AtLogger; + +public class QortalAtLoggerFactory implements org.ciyam.at.AtLoggerFactory { + + private static QortalAtLoggerFactory instance; + + private QortalAtLoggerFactory() { + } + + public static synchronized QortalAtLoggerFactory getInstance() { + if (instance == null) + instance = new QortalAtLoggerFactory(); + + return instance; + } + + @Override + public AtLogger create(final Class loggerName) { + return QortalAtLogger.create(loggerName); + } + +} diff --git a/src/main/java/org/qortal/at/QortalFunctionCode.java b/src/main/java/org/qortal/at/QortalFunctionCode.java index f7d089cf..cf6b1cfd 100644 --- a/src/main/java/org/qortal/at/QortalFunctionCode.java +++ b/src/main/java/org/qortal/at/QortalFunctionCode.java @@ -1,15 +1,18 @@ package org.qortal.at; -import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Map; import java.util.stream.Collectors; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.ciyam.at.ExecutionException; import org.ciyam.at.FunctionData; import org.ciyam.at.IllegalFunctionCodeException; import org.ciyam.at.MachineState; -import org.ciyam.at.Timestamp; +import org.qortal.crosschain.BTC; +import org.qortal.crypto.Crypto; +import org.qortal.settings.Settings; /** * Qortal-specific CIYAM-AT Functions. @@ -19,28 +22,43 @@ import org.ciyam.at.Timestamp; */ public enum QortalFunctionCode { /** - * 0x0500
- * Returns current BTC block's "timestamp" + * 0x0510
+ * Convert address in B to 20-byte value in LSB of B1, and all of B2 & B3. */ - GET_BTC_BLOCK_TIMESTAMP(0x0500, 0, true) { + CONVERT_B_TO_PKH(0x0510, 0, false) { @Override protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { - functionData.returnValue = Timestamp.toLong(state.getAPI().getCurrentBlockHeight(), BlockchainAPI.BTC.value, 0); + // Needs to be 'B' sized + byte[] pkh = new byte[32]; + + // Copy PKH part of B to last 20 bytes + System.arraycopy(getB(state), 32 - 20 - 4, pkh, 32 - 20, 20); + + setB(state, pkh); } }, /** - * 0x0501
- * Put transaction from specific recipient after timestamp in A, or zero if none
+ * 0x0511
+ * Convert 20-byte value in LSB of B1, and all of B2 & B3 to P2SH.
+ * P2SH stored in lower 25 bytes of B. */ - PUT_TX_FROM_B_RECIPIENT_AFTER_TIMESTAMP_IN_A(0x0501, 1, false) { + CONVERT_B_TO_P2SH(0x0511, 0, false) { @Override protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { - Timestamp timestamp = new Timestamp(functionData.value2); + byte addressPrefix = Settings.getInstance().getBitcoinNet() == BTC.BitcoinNet.MAIN ? 0x05 : (byte) 0xc4; - String recipient = new String(state.getB(), StandardCharsets.UTF_8); - - BlockchainAPI blockchainAPI = BlockchainAPI.valueOf(timestamp.blockchainId); - blockchainAPI.putTransactionFromRecipientAfterTimestampInA(recipient, timestamp, state); + convertAddressInB(addressPrefix, state); + } + }, + /** + * 0x0512
+ * Convert 20-byte value in LSB of B1, and all of B2 & B3 to Qortal address.
+ * Qortal address stored in lower 25 bytes of B. + */ + CONVERT_B_TO_QORTAL(0x0512, 0, false) { + @Override + protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { + convertAddressInB(Crypto.ADDRESS_VERSION, state); } }; @@ -48,7 +66,9 @@ public enum QortalFunctionCode { public final int paramCount; public final boolean returnsValue; - private final static Map map = Arrays.stream(QortalFunctionCode.values()) + private static final Logger LOGGER = LogManager.getLogger(QortalFunctionCode.class); + + private static final Map map = Arrays.stream(QortalFunctionCode.values()) .collect(Collectors.toMap(functionCode -> functionCode.value, functionCode -> functionCode)); private QortalFunctionCode(int value, int paramCount, boolean returnsValue) { @@ -61,7 +81,7 @@ public enum QortalFunctionCode { return map.get((short) value); } - public void preExecuteCheck(int paramCount, boolean returnValueExpected, MachineState state, short rawFunctionCode) throws IllegalFunctionCodeException { + public void preExecuteCheck(int paramCount, boolean returnValueExpected, short rawFunctionCode) throws IllegalFunctionCodeException { if (paramCount != this.paramCount) throw new IllegalFunctionCodeException( "Passed paramCount (" + paramCount + ") does not match function's required paramCount (" + this.paramCount + ")"); @@ -84,7 +104,7 @@ public enum QortalFunctionCode { */ public void execute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { // Check passed functionData against requirements of this function - preExecuteCheck(functionData.paramCount, functionData.returnValueExpected, state, rawFunctionCode); + preExecuteCheck(functionData.paramCount, functionData.returnValueExpected, rawFunctionCode); if (functionData.paramCount >= 1 && functionData.value1 == null) throw new IllegalFunctionCodeException("Passed value1 is null but function has paramCount of (" + this.paramCount + ")"); @@ -92,7 +112,7 @@ public enum QortalFunctionCode { if (functionData.paramCount == 2 && functionData.value2 == null) throw new IllegalFunctionCodeException("Passed value2 is null but function has paramCount of (" + this.paramCount + ")"); - state.getLogger().debug("Function \"" + this.name() + "\""); + LOGGER.debug(() -> String.format("Function \"%s\"", this.name())); postCheckExecute(functionData, state, rawFunctionCode); } @@ -100,4 +120,29 @@ public enum QortalFunctionCode { /** Actually execute function */ protected abstract void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException; + private static void convertAddressInB(byte addressPrefix, MachineState state) { + byte[] addressNoChecksum = new byte[1 + 20]; + addressNoChecksum[0] = addressPrefix; + System.arraycopy(getB(state), 0, addressNoChecksum, 1, 20); + + byte[] checksum = Crypto.doubleDigest(addressNoChecksum); + + // Needs to be 'B' sized + byte[] address = new byte[32]; + System.arraycopy(addressNoChecksum, 0, address, 32 - 1 - 20 - 4, addressNoChecksum.length); + System.arraycopy(checksum, 0, address, 32 - 4, 4); + + setB(state, address); + } + + private static byte[] getB(MachineState state) { + QortalATAPI api = (QortalATAPI) state.getAPI(); + return api.getB(state); + } + + private static void setB(MachineState state, byte[] bBytes) { + QortalATAPI api = (QortalATAPI) state.getAPI(); + api.setB(state, bBytes); + } + } diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java index a4c7131c..f49cb583 100644 --- a/src/main/java/org/qortal/block/Block.java +++ b/src/main/java/org/qortal/block/Block.java @@ -501,8 +501,10 @@ public class Block { // Allocate cache for results List transactionsData = this.repository.getBlockRepository().getTransactionsFromSignature(this.blockData.getSignature()); - // The number of transactions fetched from repository should correspond with Block's transactionCount - if (transactionsData.size() != this.blockData.getTransactionCount()) + long nonAtTransactionCount = transactionsData.stream().filter(transactionData -> transactionData.getType() != TransactionType.AT).count(); + + // The number of non-AT transactions fetched from repository should correspond with Block's transactionCount + if (nonAtTransactionCount != this.blockData.getTransactionCount()) throw new IllegalStateException("Block's transactions from repository do not match block's transaction count"); this.transactions = new ArrayList<>(); @@ -535,8 +537,10 @@ public class Block { // Allocate cache for results List atStateData = this.repository.getATRepository().getBlockATStatesAtHeight(this.blockData.getHeight()); - // The number of AT states fetched from repository should correspond with Block's atCount - if (atStateData.size() != this.blockData.getATCount()) + // The number of non-initial AT states fetched from repository should correspond with Block's atCount. + // We exclude initial AT states created by processing DEPLOY_AT transactions as they are never serialized and so not included in block's AT count. + int nonInitialCount = (int) atStateData.stream().filter(atState -> !atState.isInitial()).count(); + if (nonInitialCount != this.blockData.getATCount()) throw new IllegalStateException("Block's AT states from repository do not match block's AT count"); this.atStates = atStateData; @@ -1101,9 +1105,6 @@ public class Block { * @throws DataException */ private ValidationResult areAtsValid() throws DataException { - if (this.blockData.getATCount() == 0) - return ValidationResult.OK; - // Locally generated AT states should be valid so no need to re-execute them if (this.ourAtStates == this.getATStates()) // Note object reference compare return ValidationResult.OK; @@ -1173,7 +1174,7 @@ public class Block { // Run each AT, appends AT-Transactions and corresponding AT states, to our lists for (ATData atData : executableATs) { AT at = new AT(this.repository, atData); - List atTransactions = at.run(this.blockData.getTimestamp()); + List atTransactions = at.run(this.blockData.getHeight(), this.blockData.getTimestamp()); allAtTransactions.addAll(atTransactions); @@ -1183,17 +1184,18 @@ public class Block { this.ourAtFees += atStateData.getFees(); } + // AT Transactions never need approval + allAtTransactions.forEach(transaction -> transaction.getTransactionData().setApprovalStatus(ApprovalStatus.NOT_REQUIRED)); + // Prepend our entire AT-Transactions/states to block's transactions this.transactions.addAll(0, allAtTransactions); // Re-sort this.transactions.sort(Transaction.getComparator()); - // Update transaction count - this.blockData.setTransactionCount(this.blockData.getTransactionCount() + 1); + // AT Transactions do not affect block's transaction count - // We've added transactions, so recalculate transactions signature - calcTransactionsSignature(); + // AT Transactions do not affect block's transaction signature } /** Returns whether block's minter is actually allowed to mint this block. */ @@ -1408,13 +1410,17 @@ public class Block { protected void processAtFeesAndStates() throws DataException { ATRepository atRepository = this.repository.getATRepository(); - for (ATStateData atState : this.getATStates()) { - Account atAccount = new Account(this.repository, atState.getATAddress()); + for (ATStateData atStateData : this.ourAtStates) { + Account atAccount = new Account(this.repository, atStateData.getATAddress()); // Subtract AT-generated fees from AT accounts - atAccount.setConfirmedBalance(Asset.QORT, atAccount.getConfirmedBalance(Asset.QORT) - atState.getFees()); + atAccount.setConfirmedBalance(Asset.QORT, atAccount.getConfirmedBalance(Asset.QORT) - atStateData.getFees()); - atRepository.save(atState); + // Update AT info with latest state + ATData atData = atRepository.fromATAddress(atStateData.getATAddress()); + + AT at = new AT(repository, atData, atStateData); + at.update(this.blockData.getHeight(), this.blockData.getTimestamp()); } } @@ -1569,15 +1575,18 @@ public class Block { protected void orphanAtFeesAndStates() throws DataException { ATRepository atRepository = this.repository.getATRepository(); - for (ATStateData atState : this.getATStates()) { - Account atAccount = new Account(this.repository, atState.getATAddress()); + for (ATStateData atStateData : this.getATStates()) { + Account atAccount = new Account(this.repository, atStateData.getATAddress()); // Return AT-generated fees to AT accounts - atAccount.setConfirmedBalance(Asset.QORT, atAccount.getConfirmedBalance(Asset.QORT) + atState.getFees()); - } + atAccount.setConfirmedBalance(Asset.QORT, atAccount.getConfirmedBalance(Asset.QORT) + atStateData.getFees()); - // Delete ATStateData for this height - atRepository.deleteATStates(this.blockData.getHeight()); + // Revert AT info to prior values + ATData atData = atRepository.fromATAddress(atStateData.getATAddress()); + + AT at = new AT(repository, atData, atStateData); + at.revert(this.blockData.getHeight(), this.blockData.getTimestamp()); + } } protected void decreaseAccountLevels() throws DataException { diff --git a/src/main/java/org/qortal/block/BlockChain.java b/src/main/java/org/qortal/block/BlockChain.java index 01c09655..16f1f57d 100644 --- a/src/main/java/org/qortal/block/BlockChain.java +++ b/src/main/java/org/qortal/block/BlockChain.java @@ -151,6 +151,19 @@ public class BlockChain { /** Maximum time to retain online account signatures (ms) for block validity checks, to allow for clock variance. */ private long onlineAccountSignaturesMaxLifetime; + /** Settings relating to CIYAM AT feature. */ + public static class CiyamAtSettings { + /** Fee per step/op-code executed. */ + @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) + public long feePerStep; + /** Maximum number of steps per execution round, before AT is forced to sleep until next block. */ + public int maxStepsPerRound; + /** How many steps for calling a function. */ + public int stepsPerFunctionCall; + /** Roughly how many minutes per block. */ + public int minutesPerBlock; + } + private CiyamAtSettings ciyamAtSettings; // Constructors, etc. @@ -347,6 +360,10 @@ public class BlockChain { return this.onlineAccountSignaturesMaxLifetime; } + public CiyamAtSettings getCiyamAtSettings() { + return this.ciyamAtSettings; + } + // Convenience methods for specific blockchain feature triggers // More complex getters for aspects that change by height or timestamp @@ -406,6 +423,9 @@ public class BlockChain { if (this.founderEffectiveMintingLevel <= 0) Settings.throwValidationError("Invalid/missing \"founderEffectiveMintingLevel\" in blockchain config"); + if (this.ciyamAtSettings == null) + Settings.throwValidationError("No \"ciyamAtSettings\" entry found in blockchain config"); + if (this.featureTriggers == null) Settings.throwValidationError("No \"featureTriggers\" entry found in blockchain config"); diff --git a/src/main/java/org/qortal/block/BlockMinter.java b/src/main/java/org/qortal/block/BlockMinter.java index 3adbef3d..2026f5f7 100644 --- a/src/main/java/org/qortal/block/BlockMinter.java +++ b/src/main/java/org/qortal/block/BlockMinter.java @@ -337,11 +337,9 @@ public class BlockMinter extends Thread { this.interrupt(); } - public static void mintTestingBlock(Repository repository, PrivateKeyAccount... mintingAndOnlineAccounts) throws DataException { - if (!BlockChain.getInstance().isTestChain()) { - LOGGER.warn("Ignoring attempt to mint testing block for non-test chain!"); - return; - } + public static Block mintTestingBlock(Repository repository, PrivateKeyAccount... mintingAndOnlineAccounts) throws DataException { + if (!BlockChain.getInstance().isTestChain()) + throw new DataException("Ignoring attempt to mint testing block for non-test chain!"); // Ensure mintingAccount is 'online' so blocks can be minted Controller.getInstance().ensureTestingAccountsOnline(mintingAndOnlineAccounts); @@ -372,6 +370,8 @@ public class BlockMinter extends Thread { LOGGER.info(String.format("Minted new test block: %d", newBlock.getBlockData().getHeight())); repository.saveChanges(); + + return newBlock; } finally { blockchainLock.unlock(); } diff --git a/src/main/java/org/qortal/block/GenesisBlock.java b/src/main/java/org/qortal/block/GenesisBlock.java index 966cf884..d5fdc210 100644 --- a/src/main/java/org/qortal/block/GenesisBlock.java +++ b/src/main/java/org/qortal/block/GenesisBlock.java @@ -290,6 +290,10 @@ public class GenesisBlock extends Block { for (Transaction transaction : this.getTransactions()) this.repository.getTransactionRepository().save(transaction.getTransactionData()); + // No ATs in genesis block + this.ourAtStates = Collections.emptyList(); + this.ourAtFees = 0; + super.process(); } diff --git a/src/main/java/org/qortal/controller/Controller.java b/src/main/java/org/qortal/controller/Controller.java index 169fcda6..9b17c08b 100644 --- a/src/main/java/org/qortal/controller/Controller.java +++ b/src/main/java/org/qortal/controller/Controller.java @@ -35,6 +35,7 @@ import org.qortal.block.BlockChain; import org.qortal.block.BlockMinter; import org.qortal.block.BlockChain.BlockTimingByHeight; import org.qortal.controller.Synchronizer.SynchronizationResult; +import org.qortal.crosschain.BTC; import org.qortal.crypto.Crypto; import org.qortal.data.account.MintingAccountData; import org.qortal.data.account.RewardShareData; @@ -377,6 +378,9 @@ public class Controller extends Thread { return; // Not System.exit() so that GUI can display error } + LOGGER.info(String.format("Starting Bitcoin support using %s", Settings.getInstance().getBitcoinNet().name())); + BTC.getInstance(); + // If GUI is enabled, we're no longer starting up but actually running now Gui.getInstance().notifyRunning(); } @@ -677,6 +681,9 @@ public class Controller extends Thread { if (!isStopping) { isStopping = true; + LOGGER.info("Shutting down Bitcoin support"); + BTC.getInstance().shutdown(); + LOGGER.info("Shutting down API"); ApiService.getInstance().stop(); diff --git a/src/main/java/org/qortal/crosschain/BTC.java b/src/main/java/org/qortal/crosschain/BTC.java index 17b5cc66..caa10c36 100644 --- a/src/main/java/org/qortal/crosschain/BTC.java +++ b/src/main/java/org/qortal/crosschain/BTC.java @@ -10,96 +10,193 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; +import java.net.InetAddress; +import java.net.UnknownHostException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.security.DigestOutputStream; import java.security.MessageDigest; -import java.util.Date; +import java.security.NoSuchAlgorithmException; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.Collection; import java.util.List; +import java.util.TreeMap; import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.bitcoinj.core.Address; import org.bitcoinj.core.BlockChain; import org.bitcoinj.core.CheckpointManager; import org.bitcoinj.core.Coin; import org.bitcoinj.core.ECKey; import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.Peer; +import org.bitcoinj.core.PeerAddress; import org.bitcoinj.core.PeerGroup; import org.bitcoinj.core.Sha256Hash; import org.bitcoinj.core.StoredBlock; import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionBroadcast; import org.bitcoinj.core.TransactionOutput; -import org.bitcoinj.core.VerificationException; +import org.bitcoinj.core.listeners.BlocksDownloadedEventListener; import org.bitcoinj.core.listeners.NewBestBlockListener; import org.bitcoinj.net.discovery.DnsDiscovery; import org.bitcoinj.params.MainNetParams; +import org.bitcoinj.params.RegTestParams; import org.bitcoinj.params.TestNet3Params; -import org.bitcoinj.script.Script; +import org.bitcoinj.script.Script.ScriptType; import org.bitcoinj.store.BlockStore; import org.bitcoinj.store.BlockStoreException; -import org.bitcoinj.store.SPVBlockStore; +import org.bitcoinj.store.MemoryBlockStore; +import org.bitcoinj.utils.MonetaryFormat; import org.bitcoinj.utils.Threading; -import org.bitcoinj.wallet.KeyChainGroup; import org.bitcoinj.wallet.Wallet; +import org.bitcoinj.wallet.WalletTransaction; import org.bitcoinj.wallet.listeners.WalletCoinsReceivedEventListener; +import org.bitcoinj.wallet.listeners.WalletCoinsSentEventListener; import org.qortal.settings.Settings; public class BTC { - private static class RollbackBlockChain extends BlockChain { + public static final MonetaryFormat FORMAT = new MonetaryFormat().minDecimals(8).postfixCode(); + public static final long NO_LOCKTIME_NO_RBF_SEQUENCE = 0xFFFFFFFFL; + public static final long LOCKTIME_NO_RBF_SEQUENCE = NO_LOCKTIME_NO_RBF_SEQUENCE - 1; + public static final int HASH160_LENGTH = 20; - public RollbackBlockChain(NetworkParameters params, BlockStore blockStore) throws BlockStoreException { - super(params, blockStore); + private static final MessageDigest RIPE_MD160_DIGESTER; + private static final MessageDigest SHA256_DIGESTER; + static { + try { + RIPE_MD160_DIGESTER = MessageDigest.getInstance("RIPEMD160"); + SHA256_DIGESTER = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); } + } - @Override - public void setChainHead(StoredBlock chainHead) throws BlockStoreException { - super.setChainHead(chainHead); - } + protected static final Logger LOGGER = LogManager.getLogger(BTC.class); + public enum BitcoinNet { + MAIN { + @Override + public NetworkParameters getParams() { + return MainNetParams.get(); + } + }, + TEST3 { + @Override + public NetworkParameters getParams() { + return TestNet3Params.get(); + } + }, + REGTEST { + @Override + public NetworkParameters getParams() { + return RegTestParams.get(); + } + }; + + public abstract NetworkParameters getParams(); } private static class UpdateableCheckpointManager extends CheckpointManager implements NewBestBlockListener { + private static final long CHECKPOINT_THRESHOLD = 7 * 24 * 60 * 60; // seconds - private static final int checkpointInterval = 500; + private static final String MINIMAL_TESTNET3_TEXTFILE = "TXT CHECKPOINTS 1\n0\n1\nAAAAAAAAB+EH4QfhAAAH4AEAAAApmwX6UCEnJcYIKTa7HO3pFkqqNhAzJVBMdEuGAAAAAPSAvVCBUypCbBW/OqU0oIF7ISF84h2spOqHrFCWN9Zw6r6/T///AB0E5oOO\n"; + private static final String MINIMAL_MAINNET_TEXTFILE = "TXT CHECKPOINTS 1\n0\n1\nAAAAAAAAB+EH4QfhAAAH4AEAAABjl7tqvU/FIcDT9gcbVlA4nwtFUbxAtOawZzBpAAAAAKzkcK7NqciBjI/ldojNKncrWleVSgDfBCCn3VRrbSxXaw5/Sf//AB0z8Bkv\n"; - private static final String minimalTestNet3TextFile = "TXT CHECKPOINTS 1\n0\n1\nAAAAAAAAB+EH4QfhAAAH4AEAAAApmwX6UCEnJcYIKTa7HO3pFkqqNhAzJVBMdEuGAAAAAPSAvVCBUypCbBW/OqU0oIF7ISF84h2spOqHrFCWN9Zw6r6/T///AB0E5oOO\n"; - private static final String minimalMainNetTextFile = "TXT CHECKPOINTS 1\n0\n1\nAAAAAAAAB+EH4QfhAAAH4AEAAABjl7tqvU/FIcDT9gcbVlA4nwtFUbxAtOawZzBpAAAAAKzkcK7NqciBjI/ldojNKncrWleVSgDfBCCn3VRrbSxXaw5/Sf//AB0z8Bkv\n"; - - public UpdateableCheckpointManager(NetworkParameters params) throws IOException { - super(params, getMinimalTextFileStream(params)); + public UpdateableCheckpointManager(NetworkParameters params, File checkpointsFile) throws IOException { + super(params, getMinimalTextFileStream(params, checkpointsFile)); } public UpdateableCheckpointManager(NetworkParameters params, InputStream inputStream) throws IOException { super(params, inputStream); } - private static ByteArrayInputStream getMinimalTextFileStream(NetworkParameters params) { + private static ByteArrayInputStream getMinimalTextFileStream(NetworkParameters params, File checkpointsFile) { if (params == MainNetParams.get()) - return new ByteArrayInputStream(minimalMainNetTextFile.getBytes()); + return new ByteArrayInputStream(MINIMAL_MAINNET_TEXTFILE.getBytes()); if (params == TestNet3Params.get()) - return new ByteArrayInputStream(minimalTestNet3TextFile.getBytes()); + return new ByteArrayInputStream(MINIMAL_TESTNET3_TEXTFILE.getBytes()); + + if (params == RegTestParams.get()) + return newRegTestCheckpointsStream(checkpointsFile); // We have to build this throw new RuntimeException("Failed to construct empty UpdateableCheckpointManageer"); } - @Override - public void notifyNewBestBlock(StoredBlock block) throws VerificationException { - int height = block.getHeight(); + private static ByteArrayInputStream newRegTestCheckpointsStream(File checkpointsFile) { + try { + final NetworkParameters params = RegTestParams.get(); - if (height % checkpointInterval == 0) - checkpoints.put(block.getHeader().getTimeSeconds(), block); + final BlockStore store = new MemoryBlockStore(params); + final BlockChain chain = new BlockChain(params, store); + final PeerGroup peerGroup = new PeerGroup(params, chain); + + final InetAddress ipAddress = InetAddress.getLoopbackAddress(); + final PeerAddress peerAddress = new PeerAddress(params, ipAddress); + peerGroup.addAddress(peerAddress); + peerGroup.start(); + + final TreeMap checkpoints = new TreeMap<>(); + chain.addNewBestBlockListener((block) -> checkpoints.put(block.getHeight(), block)); + + peerGroup.downloadBlockChain(); + peerGroup.stop(); + + saveAsText(checkpointsFile, checkpoints.values()); + + return new ByteArrayInputStream(Files.readAllBytes(checkpointsFile.toPath())); + } catch (BlockStoreException e) { + throw new RuntimeException(e); + } catch (UnknownHostException e) { + throw new RuntimeException(e); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } } - public void saveAsText(File textFile) throws FileNotFoundException { + @Override + public void notifyNewBestBlock(StoredBlock block) { + final int height = block.getHeight(); + + if (height % this.params.getInterval() != 0) + return; + + final long blockTimestamp = block.getHeader().getTimeSeconds(); + final long now = System.currentTimeMillis() / 1000L; + if (blockTimestamp > now - CHECKPOINT_THRESHOLD) + return; // Too recent + + LOGGER.trace(() -> String.format("Checkpointing at block %d dated %s", height, LocalDateTime.ofInstant(Instant.ofEpochSecond(blockTimestamp), ZoneOffset.UTC))); + this.checkpoints.put(blockTimestamp, block); + + try { + saveAsText(new File(BTC.getInstance().getDirectory(), BTC.getInstance().getCheckpointsFileName()), this.checkpoints.values()); + } catch (FileNotFoundException e) { + // Save failed - log it but it's not critical + LOGGER.warn("Failed to save updated BTC checkpoints: " + e.getMessage()); + } + } + + private static void saveAsText(File textFile, Collection checkpointBlocks) throws FileNotFoundException { try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(textFile), StandardCharsets.US_ASCII))) { writer.println("TXT CHECKPOINTS 1"); writer.println("0"); // Number of signatures to read. Do this later. - writer.println(checkpoints.size()); + writer.println(checkpointBlocks.size()); + ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE); - for (StoredBlock block : checkpoints.values()) { + + for (StoredBlock block : checkpointBlocks) { block.serializeCompact(buffer); writer.println(CheckpointManager.BASE64.encode(buffer.array())); buffer.position(0); @@ -119,9 +216,11 @@ public class BTC { dataOutputStream.writeBytes("CHECKPOINTS 1"); dataOutputStream.writeInt(0); // Number of signatures to read. Do this later. digestOutputStream.on(true); - dataOutputStream.writeInt(checkpoints.size()); + dataOutputStream.writeInt(this.checkpoints.size()); + ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE); - for (StoredBlock block : checkpoints.values()) { + + for (StoredBlock block : this.checkpoints.values()) { block.serializeCompact(buffer); dataOutputStream.write(buffer.array()); buffer.position(0); @@ -130,53 +229,65 @@ public class BTC { } } } + } + private static class ResettableBlockChain extends BlockChain { + public ResettableBlockChain(NetworkParameters params, BlockStore blockStore) throws BlockStoreException { + super(params, blockStore); + } + + public void setChainHead(StoredBlock chainHead) throws BlockStoreException { + super.setChainHead(chainHead); + } } private static BTC instance; - private static final Object instanceLock = new Object(); - private static File directory; - private static String chainFileName; - private static String checkpointsFileName; + private final NetworkParameters params; + private final String checkpointsFileName; + private final File directory; - private static NetworkParameters params; - private static PeerGroup peerGroup; - private static BlockStore blockStore; - private static RollbackBlockChain chain; - private static UpdateableCheckpointManager manager; + private PeerGroup peerGroup; + private BlockStore blockStore; + private ResettableBlockChain chain; + + private UpdateableCheckpointManager manager; + + // Constructors and instance private BTC() { - // Start wallet - if (Settings.getInstance().useBitcoinTestNet()) { - params = TestNet3Params.get(); - chainFileName = "bitcoinj-testnet.spvchain"; - checkpointsFileName = "checkpoints-testnet.txt"; - } else { - params = MainNetParams.get(); - chainFileName = "bitcoinj.spvchain"; - checkpointsFileName = "checkpoints.txt"; + BitcoinNet bitcoinNet = Settings.getInstance().getBitcoinNet(); + this.params = bitcoinNet.getParams(); + + switch (bitcoinNet) { + case MAIN: + this.checkpointsFileName = "checkpoints.txt"; + break; + + case TEST3: + this.checkpointsFileName = "checkpoints-testnet.txt"; + break; + + case REGTEST: + this.checkpointsFileName = "checkpoints-regtest.txt"; + break; + + default: + throw new IllegalStateException("Unsupported Bitcoin network: " + bitcoinNet.name()); } - directory = new File("Qortal-BTC"); - if (!directory.exists()) - directory.mkdirs(); + this.directory = new File("Qortal-BTC"); - File chainFile = new File(directory, chainFileName); + if (!this.directory.exists()) + this.directory.mkdirs(); - try { - blockStore = new SPVBlockStore(params, chainFile); - } catch (BlockStoreException e) { - throw new RuntimeException("Failed to open/create BTC SPVBlockStore", e); - } - - File checkpointsFile = new File(directory, checkpointsFileName); + File checkpointsFile = new File(this.directory, this.checkpointsFileName); try (InputStream checkpointsStream = new FileInputStream(checkpointsFile)) { - manager = new UpdateableCheckpointManager(params, checkpointsStream); + this.manager = new UpdateableCheckpointManager(this.params, checkpointsStream); } catch (FileNotFoundException e) { // Construct with no checkpoints then try { - manager = new UpdateableCheckpointManager(params); + this.manager = new UpdateableCheckpointManager(this.params, checkpointsFile); } catch (IOException e2) { throw new RuntimeException("Failed to create new BTC checkpoints", e2); } @@ -185,15 +296,11 @@ public class BTC { } try { - chain = new RollbackBlockChain(params, blockStore); + this.start(System.currentTimeMillis() / 1000L); + // this.peerGroup.waitForPeers(this.peerGroup.getMaxConnections()).get(); } catch (BlockStoreException e) { - throw new RuntimeException("Failed to construct BTC blockchain", e); + throw new RuntimeException("Failed to start BTC instance", e); } - - peerGroup = new PeerGroup(params, chain); - peerGroup.setUserAgent("qortal", "1.0"); - peerGroup.addPeerDiscovery(new DnsDiscovery(params)); - peerGroup.start(); } public static synchronized BTC getInstance() { @@ -203,108 +310,259 @@ public class BTC { return instance; } - public void shutdown() { - synchronized (instanceLock) { - if (instance == null) - return; + // Getters & setters - instance = null; - } - - peerGroup.stop(); - - try { - blockStore.close(); - } catch (BlockStoreException e) { - // What can we do? - } + /* package */ File getDirectory() { + return this.directory; } + /* package */ String getCheckpointsFileName() { + return this.checkpointsFileName; + } + + public NetworkParameters getNetworkParameters() { + return this.params; + } + + // Static utility methods + + public static byte[] hash160(byte[] message) { + return RIPE_MD160_DIGESTER.digest(SHA256_DIGESTER.digest(message)); + } + + // Start-up & shutdown + private void start(long startTime) throws BlockStoreException { + StoredBlock checkpoint = this.manager.getCheckpointBefore(startTime - 1); + + this.blockStore = new MemoryBlockStore(params); + this.blockStore.put(checkpoint); + this.blockStore.setChainHead(checkpoint); + + this.chain = new ResettableBlockChain(this.params, this.blockStore); + + this.peerGroup = new PeerGroup(this.params, this.chain); + this.peerGroup.setUserAgent("qortal", "1.0"); + this.peerGroup.setPingIntervalMsec(1000L); + this.peerGroup.setMaxConnections(20); + + if (this.params != RegTestParams.get()) { + this.peerGroup.addPeerDiscovery(new DnsDiscovery(this.params)); + } else { + peerGroup.addAddress(PeerAddress.localhost(this.params)); + } + + this.peerGroup.start(); + } + + public void shutdown() { + this.peerGroup.stop(); + } + + // Utility methods + protected Wallet createEmptyWallet() { - ECKey dummyKey = new ECKey(); - - KeyChainGroup keyChainGroup = KeyChainGroup.createBasic(params); - keyChainGroup.importKeys(dummyKey); - - Wallet wallet = new Wallet(params, keyChainGroup); - - wallet.removeKey(dummyKey); - - return wallet; + return Wallet.createBasic(this.params); } - public void watch(String base58Address, long startTime) throws InterruptedException, ExecutionException, TimeoutException, BlockStoreException { - Wallet wallet = createEmptyWallet(); + private class ReplayHooks { + private Runnable preReplay; + private Runnable postReplay; - WalletCoinsReceivedEventListener coinsReceivedListener = new WalletCoinsReceivedEventListener() { - @Override - public void onCoinsReceived(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) { - System.out.println("Coins received via transaction " + tx.getTxId().toString()); - } - }; - wallet.addCoinsReceivedEventListener(coinsReceivedListener); - - Address address = Address.fromString(params, base58Address); - wallet.addWatchedAddress(address, startTime); - - StoredBlock checkpoint = manager.getCheckpointBefore(startTime); - blockStore.put(checkpoint); - blockStore.setChainHead(checkpoint); - chain.setChainHead(checkpoint); - - chain.addWallet(wallet); - peerGroup.addWallet(wallet); - peerGroup.setFastCatchupTimeSecs(startTime); - - peerGroup.addBlocksDownloadedEventListener((peer, block, filteredBlock, blocksLeft) -> { - if (blocksLeft % 1000 == 0) - System.out.println("Blocks left: " + blocksLeft); - }); - - System.out.println("Starting download..."); - peerGroup.downloadBlockChain(); - - List outputs = wallet.getWatchedOutputs(true); - - peerGroup.removeWallet(wallet); - chain.removeWallet(wallet); - - for (TransactionOutput output : outputs) - System.out.println(output.toString()); - } - - public void watch(Script script) { - // wallet.addWatchedScripts(scripts); - } - - public void updateCheckpoints() { - final long now = new Date().getTime() / 1000 - 86400; - - try { - StoredBlock checkpoint = manager.getCheckpointBefore(now); - blockStore.put(checkpoint); - blockStore.setChainHead(checkpoint); - chain.setChainHead(checkpoint); - } catch (BlockStoreException e) { - throw new RuntimeException("Failed to update BTC checkpoints", e); + public ReplayHooks(Runnable preReplay, Runnable postReplay) { + this.preReplay = preReplay; + this.postReplay = postReplay; } - peerGroup.setFastCatchupTimeSecs(now); + public void preReplay() { + this.preReplay.run(); + } - chain.addNewBestBlockListener(Threading.SAME_THREAD, manager); + public void postReplay() { + this.postReplay.run(); + } + } - peerGroup.addBlocksDownloadedEventListener((peer, block, filteredBlock, blocksLeft) -> { - if (blocksLeft % 1000 == 0) - System.out.println("Blocks left: " + blocksLeft); - }); + private void replayChain(int startTime, Wallet wallet, ReplayHooks replayHooks) throws BlockStoreException { + StoredBlock checkpoint = this.manager.getCheckpointBefore(startTime - 1); + this.blockStore.put(checkpoint); + this.blockStore.setChainHead(checkpoint); + this.chain.setChainHead(checkpoint); - System.out.println("Starting download..."); - peerGroup.downloadBlockChain(); + final WalletCoinsReceivedEventListener coinsReceivedListener = (someWallet, tx, prevBalance, newBalance) -> { + LOGGER.debug(String.format("Wallet-related transaction %s", tx.getTxId())); + }; + + final WalletCoinsSentEventListener coinsSentListener = (someWallet, tx, prevBalance, newBalance) -> { + LOGGER.debug(String.format("Wallet-related transaction %s", tx.getTxId())); + }; + + if (wallet != null) { + wallet.addCoinsReceivedEventListener(coinsReceivedListener); + wallet.addCoinsSentEventListener(coinsSentListener); + + // Link wallet to chain and peerGroup + this.chain.addWallet(wallet); + this.peerGroup.addWallet(wallet); + } try { - manager.saveAsText(new File(directory, checkpointsFileName)); - } catch (FileNotFoundException e) { - throw new RuntimeException("Failed to save updated BTC checkpoints", e); + if (replayHooks != null) + replayHooks.preReplay(); + + // Sync blockchain using peerGroup, skipping as much as we can before startTime + this.peerGroup.setFastCatchupTimeSecs(startTime); + this.chain.addNewBestBlockListener(Threading.SAME_THREAD, this.manager); + this.peerGroup.downloadBlockChain(); + } finally { + // Clean up + if (replayHooks != null) + replayHooks.postReplay(); + + if (wallet != null) { + wallet.removeCoinsReceivedEventListener(coinsReceivedListener); + wallet.removeCoinsSentEventListener(coinsSentListener); + + this.peerGroup.removeWallet(wallet); + this.chain.removeWallet(wallet); + } + + // For safety, disconnect download peer just in case + Peer downloadPeer = this.peerGroup.getDownloadPeer(); + if (downloadPeer != null) + downloadPeer.close(); + } + } + + // Actual useful methods for use by other classes + + /** Returns median timestamp from latest 11 blocks, in seconds. */ + public Long getMedianBlockTime() { + // 11 blocks, at roughly 10 minutes per block, means we should go back at least 110 minutes + // but some blocks have been way longer than 10 minutes, so be massively pessimistic + int startTime = (int) (System.currentTimeMillis() / 1000L) - 110 * 60; // 110 minutes before now, in seconds + + try { + this.replayChain(startTime, null, null); + + List latestBlocks = new ArrayList<>(11); + StoredBlock block = this.blockStore.getChainHead(); + for (int i = 0; i < 11; ++i) { + latestBlocks.add(block); + block = block.getPrev(this.blockStore); + } + + // Descending, but order shouldn't matter as we're picking median... + latestBlocks.sort((a, b) -> Long.compare(b.getHeader().getTimeSeconds(), a.getHeader().getTimeSeconds())); + + return latestBlocks.get(5).getHeader().getTimeSeconds(); + } catch (BlockStoreException e) { + LOGGER.error(String.format("BTC blockstore issue: %s", e.getMessage())); + return null; + } + } + + public Coin getBalance(String base58Address, int startTime) { + // Create new wallet containing only the address we're interested in, ignoring anything prior to startTime + Wallet wallet = createEmptyWallet(); + Address address = Address.fromString(this.params, base58Address); + wallet.addWatchedAddress(address, startTime); + + try { + replayChain(startTime, wallet, null); + + // Now that blockchain is up-to-date, return current balance + return wallet.getBalance(); + } catch (BlockStoreException e) { + LOGGER.error(String.format("BTC blockstore issue: %s", e.getMessage())); + return null; + } + } + + public List getOutputs(String base58Address, int startTime) { + Wallet wallet = createEmptyWallet(); + Address address = Address.fromString(this.params, base58Address); + wallet.addWatchedAddress(address, startTime); + + try { + replayChain(startTime, wallet, null); + + // Now that blockchain is up-to-date, return outputs + return wallet.getWatchedOutputs(true); + } catch (BlockStoreException e) { + LOGGER.error(String.format("BTC blockstore issue: %s", e.getMessage())); + return null; + } + } + + public Coin getBalanceAndOtherInfo(String base58Address, int startTime, List unspentOutputs, List walletTransactions) { + // Create new wallet containing only the address we're interested in, ignoring anything prior to startTime + Wallet wallet = createEmptyWallet(); + Address address = Address.fromString(this.params, base58Address); + wallet.addWatchedAddress(address, startTime); + + try { + replayChain(startTime, wallet, null); + + if (unspentOutputs != null) + unspentOutputs.addAll(wallet.getWatchedOutputs(true)); + + if (walletTransactions != null) + for (WalletTransaction walletTransaction : wallet.getWalletTransactions()) + walletTransactions.add(walletTransaction); + + return wallet.getBalance(); + } catch (BlockStoreException e) { + LOGGER.error(String.format("BTC blockstore issue: %s", e.getMessage())); + return null; + } + } + + public List getOutputs(byte[] txId, int startTime) { + Wallet wallet = createEmptyWallet(); + + // Add random address to wallet + ECKey fakeKey = new ECKey(); + wallet.addWatchedAddress(Address.fromKey(this.params, fakeKey, ScriptType.P2PKH), startTime); + + final Sha256Hash txHash = Sha256Hash.wrap(txId); + + final AtomicReference foundTransaction = new AtomicReference<>(); + + final BlocksDownloadedEventListener listener = (peer, block, filteredBlock, blocksLeft) -> { + List transactions = block.getTransactions(); + + if (transactions == null) + return; + + for (Transaction transaction : transactions) + if (transaction.getTxId().equals(txHash)) { + System.out.println(String.format("We downloaded block containing tx!")); + foundTransaction.set(transaction); + } + }; + + ReplayHooks replayHooks = new ReplayHooks(() -> this.peerGroup.addBlocksDownloadedEventListener(listener), () -> this.peerGroup.removeBlocksDownloadedEventListener(listener)); + + // Replay chain in the hope it will download transactionId as a dependency + try { + replayChain(startTime, wallet, replayHooks); + + Transaction realTx = foundTransaction.get(); + return realTx.getOutputs(); + } catch (BlockStoreException e) { + LOGGER.error(String.format("BTC blockstore issue: %s", e.getMessage())); + return null; + } + } + + public boolean broadcastTransaction(Transaction transaction) { + TransactionBroadcast transactionBroadcast = this.peerGroup.broadcastTransaction(transaction); + + try { + transactionBroadcast.future().get(); + return true; + } catch (InterruptedException | ExecutionException e) { + return false; } } diff --git a/src/main/java/org/qortal/crosschain/BTCACCT.java b/src/main/java/org/qortal/crosschain/BTCACCT.java new file mode 100644 index 00000000..1ba6626c --- /dev/null +++ b/src/main/java/org/qortal/crosschain/BTCACCT.java @@ -0,0 +1,663 @@ +package org.qortal.crosschain; + +import static org.ciyam.at.OpCode.calcOffset; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.Transaction.SigHash; +import org.bitcoinj.core.TransactionInput; +import org.bitcoinj.core.TransactionOutput; +import org.bitcoinj.crypto.TransactionSignature; +import org.bitcoinj.script.Script; +import org.bitcoinj.script.ScriptBuilder; +import org.bitcoinj.script.ScriptChunk; +import org.bitcoinj.script.ScriptOpCodes; +import org.bitcoinj.script.Script.ScriptType; +import org.bitcoinj.wallet.WalletTransaction; +import org.ciyam.at.API; +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.qortal.account.Account; +import org.qortal.asset.Asset; +import org.qortal.at.QortalAtLoggerFactory; +import org.qortal.block.BlockChain; +import org.qortal.block.BlockChain.CiyamAtSettings; +import org.qortal.crypto.Crypto; +import org.qortal.data.at.ATData; +import org.qortal.data.at.ATStateData; +import org.qortal.data.block.BlockData; +import org.qortal.data.crosschain.CrossChainTradeData; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.utils.Base58; +import org.qortal.utils.BitTwiddling; + +import com.google.common.hash.HashCode; +import com.google.common.primitives.Bytes; + +/* + * Bob generates Bitcoin private key + * private key required to sign P2SH redeem tx + * private key can be used to create 'secret' (e.g. double-SHA256) + * encrypted private key could be stored in Qortal AT for access by Bob from any node + * Bob creates Qortal AT + * Alice finds Qortal AT and wants to trade + * Alice generates Bitcoin private key + * Alice will need to send Bob her Qortal address and Bitcoin refund address + * Bob sends Alice's Qortal address to Qortal AT + * Qortal AT sends initial QORT payment to Alice (so she has QORT to send message to AT and claim funds) + * Alice receives funds and checks Qortal AT to confirm it's locked to her + * Alice creates/funds Bitcoin P2SH + * Alice requires: Bob's redeem Bitcoin address, Alice's refund Bitcoin address, derived locktime + * Bob checks P2SH is funded + * Bob requires: Bob's redeem Bitcoin address, Alice's refund Bitcoin address, derived locktime + * Bob uses secret to redeem P2SH + * Qortal core/UI will need to create, and sign, this transaction + * Alice scans P2SH redeem tx and uses secret to redeem Qortal AT + */ + +public class BTCACCT { + + public static final int SECRET_LENGTH = 32; + public static final int MIN_LOCKTIME = 1500000000; + public static final byte[] CODE_BYTES_HASH = HashCode.fromString("edcdb1feb36e079c5f956faff2f24219b12e5fbaaa05654335e615e33218282f").asBytes(); // SHA256 of AT code bytes + + /* + * OP_TUCK (to copy public key to before signature) + * OP_CHECKSIGVERIFY (sig & pubkey must verify or script fails) + * OP_HASH160 (convert public key to PKH) + * OP_DUP (duplicate PKH) + * OP_EQUAL (does PKH match refund PKH?) + * OP_IF + * OP_DROP (no need for duplicate PKH) + * + * OP_CHECKLOCKTIMEVERIFY (if this passes, leftover stack is so script passes) + * OP_ELSE + * OP_EQUALVERIFY (duplicate PKH must match redeem PKH or script fails) + * OP_HASH160 (hash secret) + * OP_EQUAL (do hashes of secrets match? if true, script passes else script fails) + * OP_ENDIF + */ + + private static final byte[] redeemScript1 = HashCode.fromString("7dada97614").asBytes(); // OP_TUCK OP_CHECKSIGVERIFY OP_HASH160 OP_DUP push(0x14 bytes) + private static final byte[] redeemScript2 = HashCode.fromString("87637504").asBytes(); // OP_EQUAL OP_IF OP_DROP push(0x4 bytes) + private static final byte[] redeemScript3 = HashCode.fromString("b16714").asBytes(); // OP_CHECKLOCKTIMEVERIFY OP_ELSE push(0x14 bytes) + private static final byte[] redeemScript4 = HashCode.fromString("88a914").asBytes(); // OP_EQUALVERIFY OP_HASH160 push(0x14 bytes) + private static final byte[] redeemScript5 = HashCode.fromString("8768").asBytes(); // OP_EQUAL OP_ENDIF + + /** + * Returns Bitcoin redeemScript used for cross-chain trading. + *

+ * See comments in {@link BTCACCT} for more details. + * + * @param refunderPubKeyHash 20-byte HASH160 of P2SH funder's public key, for refunding purposes + * @param lockTime seconds-since-epoch threshold, after which P2SH funder can claim refund + * @param redeemerPubKeyHash 20-byte HASH160 of P2SH redeemer's public key + * @param secretHash 20-byte HASH160 of secret, used by P2SH redeemer to claim funds + * @return + */ + public static byte[] buildScript(byte[] refunderPubKeyHash, int lockTime, byte[] redeemerPubKeyHash, byte[] secretHash) { + return Bytes.concat(redeemScript1, refunderPubKeyHash, redeemScript2, BitTwiddling.toLEByteArray((int) (lockTime & 0xffffffffL)), + redeemScript3, redeemerPubKeyHash, redeemScript4, secretHash, redeemScript5); + } + + /** + * Builds a custom transaction to spend P2SH. + * + * @param amount output amount, should be total of input amounts, less miner fees + * @param spendKey key for signing transaction, and also where funds are 'sent' (output) + * @param fundingOutput output from transaction that funded P2SH address + * @param redeemScriptBytes the redeemScript itself, in byte[] form + * @param lockTime (optional) transaction nLockTime, used in refund scenario + * @param scriptSigBuilder function for building scriptSig using transaction input signature + * @return Signed Bitcoin transaction for spending P2SH + */ + public static Transaction buildP2shTransaction(Coin amount, ECKey spendKey, TransactionOutput fundingOutput, byte[] redeemScriptBytes, Long lockTime, Function scriptSigBuilder) { + NetworkParameters params = BTC.getInstance().getNetworkParameters(); + + Transaction transaction = new Transaction(params); + transaction.setVersion(2); + + // Output is back to P2SH funder + transaction.addOutput(amount, ScriptBuilder.createP2PKHOutputScript(spendKey.getPubKeyHash())); + + // Input (without scriptSig prior to signing) + TransactionInput input = new TransactionInput(params, null, redeemScriptBytes, fundingOutput.getOutPointFor()); + if (lockTime != null) + input.setSequenceNumber(BTC.LOCKTIME_NO_RBF_SEQUENCE); // Use max-value, so no lockTime and no RBF + else + input.setSequenceNumber(BTC.NO_LOCKTIME_NO_RBF_SEQUENCE); // Use max-value - 1, so lockTime can be used but not RBF + transaction.addInput(input); + + // Set locktime after inputs added but before input signatures are generated + if (lockTime != null) + transaction.setLockTime(lockTime); + + // Generate transaction signature for input + final boolean anyoneCanPay = false; + TransactionSignature txSig = transaction.calculateSignature(0, spendKey, redeemScriptBytes, SigHash.ALL, anyoneCanPay); + + // Calculate transaction signature + byte[] txSigBytes = txSig.encodeToBitcoin(); + + // Build scriptSig using lambda and tx signature + Script scriptSig = scriptSigBuilder.apply(txSigBytes); + + // Set input scriptSig + transaction.getInput(0).setScriptSig(scriptSig); + + return transaction; + } + + /** + * Returns signed Bitcoin transaction claiming refund from P2SH address. + * + * @param refundAmount refund amount, should be total of input amounts, less miner fees + * @param refundKey key for signing transaction, and also where refund is 'sent' (output) + * @param fundingOutput output from transaction that funded P2SH address + * @param redeemScriptBytes the redeemScript itself, in byte[] form + * @param lockTime transaction nLockTime - must be at least locktime used in redeemScript + * @return Signed Bitcoin transaction for refunding P2SH + */ + public static Transaction buildRefundTransaction(Coin refundAmount, ECKey refundKey, TransactionOutput fundingOutput, byte[] redeemScriptBytes, long lockTime) { + Function refundSigScriptBuilder = (txSigBytes) -> { + // Build scriptSig with... + ScriptBuilder scriptBuilder = new ScriptBuilder(); + + // transaction signature + scriptBuilder.addChunk(new ScriptChunk(txSigBytes.length, txSigBytes)); + + // redeem public key + byte[] refundPubKey = refundKey.getPubKey(); + scriptBuilder.addChunk(new ScriptChunk(refundPubKey.length, refundPubKey)); + + // redeem script + scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes)); + + return scriptBuilder.build(); + }; + + return buildP2shTransaction(refundAmount, refundKey, fundingOutput, redeemScriptBytes, lockTime, refundSigScriptBuilder); + } + + /** + * Returns signed Bitcoin transaction redeeming funds from P2SH address. + * + * @param redeemAmount redeem amount, should be total of input amounts, less miner fees + * @param redeemKey key for signing transaction, and also where funds are 'sent' (output) + * @param fundingOutput output from transaction that funded P2SH address + * @param redeemScriptBytes the redeemScript itself, in byte[] form + * @param secret actual 32-byte secret used when building redeemScript + * @return Signed Bitcoin transaction for redeeming P2SH + */ + public static Transaction buildRedeemTransaction(Coin redeemAmount, ECKey redeemKey, TransactionOutput fundingOutput, byte[] redeemScriptBytes, byte[] secret) { + Function redeemSigScriptBuilder = (txSigBytes) -> { + // Build scriptSig with... + ScriptBuilder scriptBuilder = new ScriptBuilder(); + + // secret + scriptBuilder.addChunk(new ScriptChunk(secret.length, secret)); + + // transaction signature + scriptBuilder.addChunk(new ScriptChunk(txSigBytes.length, txSigBytes)); + + // redeem public key + byte[] redeemPubKey = redeemKey.getPubKey(); + scriptBuilder.addChunk(new ScriptChunk(redeemPubKey.length, redeemPubKey)); + + // redeem script + scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes)); + + return scriptBuilder.build(); + }; + + return buildP2shTransaction(redeemAmount, redeemKey, fundingOutput, redeemScriptBytes, null, redeemSigScriptBuilder); + } + + /** + * Returns Qortal AT creation bytes for cross-chain trading AT. + *

+ * tradeTimeout (minutes) is the time window for the recipient to send the + * 32-byte secret to the AT, before the AT automatically refunds the AT's creator. + * + * @param qortalCreator Qortal address for AT creator, also used for refunds + * @param secretHash 20-byte HASH160 of 32-byte secret + * @param tradeTimeout how many minutes, from start of 'trade mode' until AT auto-refunds AT creator + * @param initialPayout how much QORT to pay trade partner upon switch to 'trade mode' + * @param redeemPayout how much QORT to pay trade partner if they send correct 32-byte secret to AT + * @param bitcoinAmount how much BTC the AT creator is expecting to trade + * @return + */ + public static byte[] buildQortalAT(String qortalCreator, byte[] secretHash, int tradeTimeout, long initialPayout, long redeemPayout, long bitcoinAmount) { + // Labels for data segment addresses + int addrCounter = 0; + + // Constants (with corresponding dataByteBuffer.put*() calls below) + + final int addrQortalCreator1 = addrCounter++; + final int addrQortalCreator2 = addrCounter++; + final int addrQortalCreator3 = addrCounter++; + final int addrQortalCreator4 = addrCounter++; + + final int addrSecretHash = addrCounter; + addrCounter += 4; + + final int addrTradeTimeout = addrCounter++; + final int addrInitialPayoutAmount = addrCounter++; + final int addrRedeemPayoutAmount = addrCounter++; + final int addrBitcoinAmount = addrCounter++; + + final int addrMessageTxType = addrCounter++; + + final int addrSecretHashPointer = addrCounter++; + final int addrQortalRecipientPointer = addrCounter++; + final int addrMessageSenderPointer = addrCounter++; + + final int addrMessageDataPointer = addrCounter++; + final int addrMessageDataLength = addrCounter++; + + final int addrEndOfConstants = addrCounter; + + // Variables + + final int addrQortalRecipient1 = addrCounter++; + final int addrQortalRecipient2 = addrCounter++; + final int addrQortalRecipient3 = addrCounter++; + final int addrQortalRecipient4 = addrCounter++; + + final int addrTradeRefundTimestamp = addrCounter++; + final int addrLastTxTimestamp = addrCounter++; + final int addrBlockTimestamp = addrCounter++; + final int addrTxType = addrCounter++; + final int addrResult = addrCounter++; + + final int addrMessageSender1 = addrCounter++; + final int addrMessageSender2 = addrCounter++; + final int addrMessageSender3 = addrCounter++; + final int addrMessageSender4 = addrCounter++; + + final int addrMessageData = addrCounter; + addrCounter += 4; + + // Data segment + ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE); + + // AT creator's Qortal address, decoded from Base58 + assert dataByteBuffer.position() == addrQortalCreator1 * MachineState.VALUE_SIZE : "addrQortalCreator1 incorrect"; + byte[] qortalCreatorBytes = Base58.decode(qortalCreator); + dataByteBuffer.put(Bytes.ensureCapacity(qortalCreatorBytes, 32, 0)); + + // Hash of secret + assert dataByteBuffer.position() == addrSecretHash * MachineState.VALUE_SIZE : "addrSecretHash incorrect"; + dataByteBuffer.put(Bytes.ensureCapacity(secretHash, 32, 0)); + + // Trade timeout in minutes + assert dataByteBuffer.position() == addrTradeTimeout * MachineState.VALUE_SIZE : "addrTradeTimeout incorrect"; + dataByteBuffer.putLong(tradeTimeout); + + // Initial payout amount + assert dataByteBuffer.position() == addrInitialPayoutAmount * MachineState.VALUE_SIZE : "addrInitialPayoutAmount incorrect"; + dataByteBuffer.putLong(initialPayout); + + // Redeem payout amount + assert dataByteBuffer.position() == addrRedeemPayoutAmount * MachineState.VALUE_SIZE : "addrRedeemPayoutAmount incorrect"; + dataByteBuffer.putLong(redeemPayout); + + // Expected Bitcoin amount + assert dataByteBuffer.position() == addrBitcoinAmount * MachineState.VALUE_SIZE : "addrBitcoinAmount incorrect"; + dataByteBuffer.putLong(bitcoinAmount); + + // We're only interested in MESSAGE transactions + assert dataByteBuffer.position() == addrMessageTxType * MachineState.VALUE_SIZE : "addrMessageTxType incorrect"; + dataByteBuffer.putLong(API.ATTransactionType.MESSAGE.value); + + // Index into data segment of hash, used by GET_B_IND + assert dataByteBuffer.position() == addrSecretHashPointer * MachineState.VALUE_SIZE : "addrSecretHashPointer incorrect"; + dataByteBuffer.putLong(addrSecretHash); + + // Index into data segment of recipient address, used by SET_B_IND + assert dataByteBuffer.position() == addrQortalRecipientPointer * MachineState.VALUE_SIZE : "addrQortalRecipientPointer incorrect"; + dataByteBuffer.putLong(addrQortalRecipient1); + + // 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); + + // 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); + + assert dataByteBuffer.position() == addrEndOfConstants * MachineState.VALUE_SIZE : "dataByteBuffer position not at end of constants"; + + // Code labels + Integer labelRefund = null; + + Integer labelOfferTxLoop = null; + Integer labelCheckOfferTx = null; + + Integer labelTradeMode = null; + Integer labelTradeTxLoop = null; + Integer labelCheckTradeTx = null; + + 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 from AT owner containing trade partner details, or AT owner's address to cancel offer */ + + /* Transaction processing loop */ + labelOfferTxLoop = 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, addrLastTxTimestamp)); + // 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, labelCheckOfferTx))); + // Stop and wait for next block + codeByteBuffer.put(OpCode.STP_IMD.compile()); + + /* Check transaction */ + labelCheckOfferTx = 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, addrLastTxTimestamp)); + // Extract transaction type (message/payment) from transaction and save type in addrTxType + codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TYPE_FROM_TX_IN_A, addrTxType)); + // If transaction type is not MESSAGE type then go look for another transaction + codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxType, addrMessageTxType, calcOffset(codeByteBuffer, labelOfferTxLoop))); + + /* Check transaction's sender */ + + // 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, addrQortalCreator1, calcOffset(codeByteBuffer, labelOfferTxLoop))); + codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrQortalCreator2, calcOffset(codeByteBuffer, labelOfferTxLoop))); + codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrQortalCreator3, calcOffset(codeByteBuffer, labelOfferTxLoop))); + codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrQortalCreator4, calcOffset(codeByteBuffer, labelOfferTxLoop))); + + /* Extract trade partner info from message */ + + // 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 addrQortalRecipient1 (as pointed to by addrQortalRecipientPointer) + codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrQortalRecipientPointer)); + // Compare each of recipient address with creator's address (for offer-cancel scenario). If they don't match, assume recipient is trade partner. + codeByteBuffer.put(OpCode.BNE_DAT.compile(addrQortalRecipient1, addrQortalCreator1, calcOffset(codeByteBuffer, labelTradeMode))); + codeByteBuffer.put(OpCode.BNE_DAT.compile(addrQortalRecipient2, addrQortalCreator2, calcOffset(codeByteBuffer, labelTradeMode))); + codeByteBuffer.put(OpCode.BNE_DAT.compile(addrQortalRecipient3, addrQortalCreator3, calcOffset(codeByteBuffer, labelTradeMode))); + codeByteBuffer.put(OpCode.BNE_DAT.compile(addrQortalRecipient4, addrQortalCreator4, calcOffset(codeByteBuffer, labelTradeMode))); + // Recipient address is AT creator's address, so cancel offer and finish. + codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRefund == null ? 0 : labelRefund)); + + /* Switch to 'trade mode' */ + labelTradeMode = codeByteBuffer.position(); + + // Send initial payment to recipient so they have enough funds to message AT if all goes well + codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PAY_TO_ADDRESS_IN_B, addrInitialPayoutAmount)); + + // Calculate trade timeout refund 'timestamp' by adding addrTradeTimeout minutes to above message's 'timestamp', then save into addrTradeRefundTimestamp + codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.ADD_MINUTES_TO_TIMESTAMP, addrTradeRefundTimestamp, addrLastTxTimestamp, addrTradeTimeout)); + + // Set restart position to after this opcode + codeByteBuffer.put(OpCode.SET_PCS.compile()); + + /* Loop, waiting for trade timeout or message from Qortal trade recipient containing secret */ + + // 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, addrTradeRefundTimestamp, calcOffset(codeByteBuffer, labelTradeTxLoop))); + // 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 */ + labelTradeTxLoop = 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, addrLastTxTimestamp)); + // 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, labelCheckTradeTx))); + // Stop and wait for next block + codeByteBuffer.put(OpCode.STP_IMD.compile()); + + /* Check transaction */ + labelCheckTradeTx = 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, addrLastTxTimestamp)); + // Extract transaction type (message/payment) from transaction and save type in addrTxType + codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TYPE_FROM_TX_IN_A, addrTxType)); + // If transaction type is not MESSAGE type then go look for another transaction + codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxType, addrMessageTxType, calcOffset(codeByteBuffer, labelTradeTxLoop))); + + /* Check transaction's sender */ + + // 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, addrQortalRecipient1, calcOffset(codeByteBuffer, labelTradeTxLoop))); + codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrQortalRecipient2, calcOffset(codeByteBuffer, labelTradeTxLoop))); + codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrQortalRecipient3, calcOffset(codeByteBuffer, labelTradeTxLoop))); + codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrQortalRecipient4, calcOffset(codeByteBuffer, labelTradeTxLoop))); + + /* Check 'secret' in transaction's message */ + + // 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 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 addrSecretHashPointer) + codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrSecretHashPointer)); + // 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.BZR_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelTradeTxLoop))); + + /* Success! Pay arranged amount to intended recipient */ + + // Load B register with intended recipient address (as pointed to by addrQortalRecipientPointer) + codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrQortalRecipientPointer)); + // Pay AT's balance to recipient + codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PAY_TO_ADDRESS_IN_B, addrRedeemPayoutAmount)); + // Fall-through to refunding any remaining balance back to AT creator + + /* Refund balance back to AT creator */ + labelRefund = codeByteBuffer.position(); + + // Load B register with AT creator's address. + codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_CREATOR_INTO_B)); + // Pay AT's balance back to AT's creator. + codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PAY_ALL_TO_ADDRESS_IN_B)); + // We're finished forever + codeByteBuffer.put(OpCode.FIN_IMD.compile()); + } catch (CompilationException e) { + throw new IllegalStateException("Unable to compile BTC-QORT ACCT?", e); + } + } + + codeByteBuffer.flip(); + + byte[] codeBytes = new byte[codeByteBuffer.limit()]; + codeByteBuffer.get(codeBytes); + + assert Arrays.equals(Crypto.digest(codeBytes), BTCACCT.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. + * + * @param repository + * @param atAddress + * @throws DataException + */ + public static CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException { + String atAddress = atData.getATAddress(); + + ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress); + byte[] stateData = atStateData.getStateData(); + + QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance(); + byte[] dataBytes = MachineState.extractDataBytes(loggerFactory, stateData); + + CrossChainTradeData tradeData = new CrossChainTradeData(); + tradeData.qortalAtAddress = atAddress; + tradeData.qortalCreator = Crypto.toAddress(atData.getCreatorPublicKey()); + tradeData.creationTimestamp = atData.getCreation(); + tradeData.qortBalance = repository.getAccountRepository().getBalance(atAddress, Asset.QORT).getBalance(); + + ByteBuffer dataByteBuffer = ByteBuffer.wrap(dataBytes); + byte[] addressBytes = new byte[32]; + + // Skip AT creator address + dataByteBuffer.position(dataByteBuffer.position() + 32); + + // Hash of secret + tradeData.secretHash = new byte[20]; + dataByteBuffer.get(tradeData.secretHash); + dataByteBuffer.position(dataByteBuffer.position() + 32 - 20); // skip to 32 bytes + + // Trade timeout + tradeData.tradeRefundTimeout = dataByteBuffer.getLong(); + + // Initial payout + tradeData.initialPayout = dataByteBuffer.getLong(); + + // Redeem payout + tradeData.redeemPayout = dataByteBuffer.getLong(); + + // Expected BTC amount + tradeData.expectedBitcoin = dataByteBuffer.getLong(); + + // Skip MESSAGE transaction type + dataByteBuffer.position(dataByteBuffer.position() + 8); + + // Skip pointer to secretHash + dataByteBuffer.position(dataByteBuffer.position() + 8); + + // Skip pointer to Qortal recipient + dataByteBuffer.position(dataByteBuffer.position() + 8); + + // Skip pointer to message sender + dataByteBuffer.position(dataByteBuffer.position() + 8); + + // Skip pointer to message data + dataByteBuffer.position(dataByteBuffer.position() + 8); + + // Skip message data length + dataByteBuffer.position(dataByteBuffer.position() + 8); + + // Qortal recipient (if any) + dataByteBuffer.get(addressBytes); + + // Trade offer timeout (AT 'timestamp' converted to Qortal block height) + long tradeRefundTimestamp = dataByteBuffer.getLong(); + + if (tradeRefundTimestamp != 0) { + tradeData.mode = CrossChainTradeData.Mode.TRADE; + tradeData.tradeRefundHeight = new Timestamp(tradeRefundTimestamp).blockHeight; + + if (addressBytes[0] != 0) + tradeData.qortalRecipient = Base58.encode(Arrays.copyOf(addressBytes, Account.ADDRESS_LENGTH)); + + // We'll suggest half of trade timeout + CiyamAtSettings ciyamAtSettings = BlockChain.getInstance().getCiyamAtSettings(); + + int tradeModeSwitchHeight = (int) (tradeData.tradeRefundHeight - tradeData.tradeRefundTimeout / ciyamAtSettings.minutesPerBlock); + + BlockData blockData = repository.getBlockRepository().fromHeight(tradeModeSwitchHeight); + if (blockData != null) { + tradeData.tradeModeTimestamp = blockData.getTimestamp(); // NOTE: milliseconds from epoch + tradeData.lockTime = (int) (tradeData.tradeModeTimestamp / 1000L + tradeData.tradeRefundTimeout / 2 * 60); + } + } else { + tradeData.mode = CrossChainTradeData.Mode.OFFER; + } + + return tradeData; + } + + public static byte[] findP2shSecret(String p2shAddress, List walletTransactions) { + NetworkParameters params = BTC.getInstance().getNetworkParameters(); + + for (WalletTransaction walletTransaction : walletTransactions) { + Transaction transaction = walletTransaction.getTransaction(); + + // Cycle through inputs, looking for one that spends our P2SH + for (TransactionInput input : transaction.getInputs()) { + TransactionOutput connectedOutput = input.getConnectedOutput(); + if (connectedOutput == null) + // We don't know about this transaction that this input is spending, so won't be our P2SH + continue; + + Script scriptPubKey = connectedOutput.getScriptPubKey(); + ScriptType scriptType = scriptPubKey.getScriptType(); + if (scriptType != ScriptType.P2SH) + // Input isn't spending our P2SH + continue; + + Address inputAddress = scriptPubKey.getToAddress(params); + if (!inputAddress.toString().equals(p2shAddress)) + // Input isn't spending our P2SH + continue; + + Script scriptSig = input.getScriptSig(); + List scriptChunks = scriptSig.getChunks(); + + // Expected number of script chunks + int expectedChunkCount = 1 /* secret */ + 1 /* sig */ + 1 /* pubkey */ + 1 /* redeemScript */; + if (scriptChunks.size() != expectedChunkCount) + continue; + + byte[] secret = scriptChunks.get(0).data; + if (secret.length != BTCACCT.SECRET_LENGTH) + continue; + + return secret; + } + } + + return null; + } + +} diff --git a/src/main/java/org/qortal/crypto/Crypto.java b/src/main/java/org/qortal/crypto/Crypto.java index cce924a7..1b7df86e 100644 --- a/src/main/java/org/qortal/crypto/Crypto.java +++ b/src/main/java/org/qortal/crypto/Crypto.java @@ -58,6 +58,18 @@ public class Crypto { return Bytes.concat(digest, digest); } + /** Returns RMD160(SHA256(data)) */ + public static byte[] hash160(byte[] data) { + byte[] interim = digest(data); + + try { + MessageDigest md160 = MessageDigest.getInstance("RIPEMD160"); + return md160.digest(interim); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("RIPEMD160 message digest not available"); + } + } + private static String toAddress(byte addressVersion, byte[] input) { // SHA2-256 input to create new data and of known size byte[] inputHash = digest(input); @@ -100,14 +112,15 @@ public class Crypto { return isValidTypedAddress(address, ADDRESS_VERSION, AT_ADDRESS_VERSION); } + public static boolean isValidAddress(byte[] addressBytes) { + return areValidTypedAddressBytes(addressBytes, ADDRESS_VERSION, AT_ADDRESS_VERSION); + } + public static boolean isValidAtAddress(String address) { return isValidTypedAddress(address, AT_ADDRESS_VERSION); } private static boolean isValidTypedAddress(String address, byte...addressVersions) { - if (addressVersions == null || addressVersions.length == 0) - return false; - byte[] addressBytes; try { @@ -117,6 +130,13 @@ public class Crypto { return false; } + return areValidTypedAddressBytes(addressBytes, addressVersions); + } + + private static boolean areValidTypedAddressBytes(byte[] addressBytes, byte...addressVersions) { + if (addressVersions == null || addressVersions.length == 0) + return false; + // Check address length if (addressBytes == null || addressBytes.length != Account.ADDRESS_LENGTH) return false; diff --git a/src/main/java/org/qortal/data/at/ATData.java b/src/main/java/org/qortal/data/at/ATData.java index 755c6caf..02f79f84 100644 --- a/src/main/java/org/qortal/data/at/ATData.java +++ b/src/main/java/org/qortal/data/at/ATData.java @@ -1,5 +1,11 @@ package org.qortal.data.at; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; + +// All properties to be converted to JSON via JAXB +@XmlAccessorType(XmlAccessType.FIELD) public class ATData { // Properties @@ -9,23 +15,30 @@ public class ATData { private int version; private long assetId; private byte[] codeBytes; + private byte[] codeHash; private boolean isSleeping; private Integer sleepUntilHeight; private boolean isFinished; private boolean hadFatalError; private boolean isFrozen; + @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) private Long frozenBalance; // Constructors - public ATData(String ATAddress, byte[] creatorPublicKey, long creation, int version, long assetId, byte[] codeBytes, boolean isSleeping, - Integer sleepUntilHeight, boolean isFinished, boolean hadFatalError, boolean isFrozen, Long frozenBalance) { + // necessary for JAXB serialization + protected 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) { this.ATAddress = ATAddress; this.creatorPublicKey = creatorPublicKey; this.creation = creation; this.version = version; this.assetId = assetId; this.codeBytes = codeBytes; + this.codeHash = codeHash; this.isSleeping = isSleeping; this.sleepUntilHeight = sleepUntilHeight; this.isFinished = isFinished; @@ -34,6 +47,14 @@ public class ATData { this.frozenBalance = frozenBalance; } + /** For constructing skeleton ATData with bare minimum info. */ + public ATData(String ATAddress, byte[] creatorPublicKey, long creation, long assetId) { + this.ATAddress = ATAddress; + this.creatorPublicKey = creatorPublicKey; + this.creation = creation; + this.assetId = assetId; + } + // Getters / setters public String getATAddress() { @@ -60,6 +81,10 @@ public class ATData { return this.codeBytes; } + public byte[] getCodeHash() { + return this.codeHash; + } + public boolean getIsSleeping() { return this.isSleeping; } diff --git a/src/main/java/org/qortal/data/at/ATStateData.java b/src/main/java/org/qortal/data/at/ATStateData.java index a1131b4b..b8c13e0d 100644 --- a/src/main/java/org/qortal/data/at/ATStateData.java +++ b/src/main/java/org/qortal/data/at/ATStateData.java @@ -9,32 +9,38 @@ public class ATStateData { private byte[] stateData; private byte[] stateHash; private Long fees; + private boolean isInitial; // Constructors /** Create new ATStateData */ - public ATStateData(String ATAddress, Integer height, Long creation, byte[] stateData, byte[] stateHash, Long fees) { + public ATStateData(String ATAddress, Integer height, Long creation, byte[] stateData, byte[] stateHash, Long fees, boolean isInitial) { this.ATAddress = ATAddress; this.height = height; this.creation = creation; this.stateData = stateData; this.stateHash = stateHash; this.fees = fees; + this.isInitial = isInitial; } /** For recreating per-block ATStateData from repository where not all info is needed */ - public ATStateData(String ATAddress, int height, byte[] stateHash, Long fees) { - this(ATAddress, height, null, null, stateHash, fees); + public ATStateData(String ATAddress, int height, byte[] stateHash, Long fees, boolean isInitial) { + this(ATAddress, height, null, 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(ATAddress, null, null, null, stateHash, null); + // 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, null, stateHash, null, false); } /** For creating ATStateData from serialized bytes when we don't have all the info */ public ATStateData(String ATAddress, byte[] stateHash, Long fees) { - this(ATAddress, null, null, null, stateHash, 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, null, stateHash, fees, false); } // Getters / setters @@ -68,4 +74,8 @@ public class ATStateData { return this.fees; } + public boolean isInitial() { + return this.isInitial; + } + } diff --git a/src/main/java/org/qortal/data/crosschain/CrossChainTradeData.java b/src/main/java/org/qortal/data/crosschain/CrossChainTradeData.java new file mode 100644 index 00000000..8c9b6602 --- /dev/null +++ b/src/main/java/org/qortal/data/crosschain/CrossChainTradeData.java @@ -0,0 +1,68 @@ +package org.qortal.data.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; + +// All properties to be converted to JSON via JAXB +@XmlAccessorType(XmlAccessType.FIELD) +public class CrossChainTradeData { + + public enum Mode { OFFER, TRADE }; + + // Properties + + @Schema(description = "AT's Qortal address") + public String qortalAtAddress; + + @Schema(description = "AT creator's Qortal address") + public String qortalCreator; + + @Schema(description = "Timestamp when AT was created (milliseconds since epoch)") + public long creationTimestamp; + + @Schema(description = "AT's current QORT balance") + @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) + public long qortBalance; + + @Schema(description = "HASH160 of 32-byte secret") + public byte[] secretHash; + + @Schema(description = "Initial QORT payment that will be sent to Qortal trade partner") + @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) + public long initialPayout; + + @Schema(description = "Final QORT payment that will be sent to Qortal trade partner") + @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) + public long redeemPayout; + + @Schema(description = "Trade partner's Qortal address (trade begins when this is set)") + public String qortalRecipient; + + @Schema(description = "Timestamp when AT switched to trade mode") + public Long tradeModeTimestamp; + + @Schema(description = "How long from beginning trade until AT triggers automatic refund to AT creator (minutes)") + public long tradeRefundTimeout; + + @Schema(description = "Actual Qortal block height when AT will automatically refund to AT creator (after trade begins)") + public Integer tradeRefundHeight; + + @Schema(description = "Amount, in BTC, that AT creator expects Bitcoin P2SH to pay out (excluding miner fees)") + @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) + public long expectedBitcoin; + + public Mode mode; + + @Schema(description = "Suggested Bitcoin P2SH nLockTime based on trade timeout") + public Integer lockTime; + + // Constructors + + // Necessary for JAXB + public CrossChainTradeData() { + } + +} diff --git a/src/main/java/org/qortal/repository/ATRepository.java b/src/main/java/org/qortal/repository/ATRepository.java index 9653fc6c..affbaf18 100644 --- a/src/main/java/org/qortal/repository/ATRepository.java +++ b/src/main/java/org/qortal/repository/ATRepository.java @@ -18,6 +18,9 @@ public interface ATRepository { /** Returns list of executable ATs, empty if none found */ public List getAllExecutableATs() throws DataException; + /** Returns list of ATs with matching code hash, optionally executable only. */ + public List getATsByFunctionality(byte[] codeHash, Boolean isExecutable, Integer limit, Integer offset, Boolean reverse) throws DataException; + /** Returns creation block height given AT's address or null if not found */ public Integer getATCreationBlockHeight(String atAddress) throws DataException; diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java index 7b399422..feae3b5c 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java @@ -1,10 +1,11 @@ package org.qortal.repository.hsqldb; +import static org.qortal.repository.hsqldb.HSQLDBRepository.getZonedTimestampMilli; +import static org.qortal.repository.hsqldb.HSQLDBRepository.toOffsetDateTime; + import java.sql.ResultSet; import java.sql.SQLException; -import java.sql.Timestamp; import java.util.ArrayList; -import java.util.Calendar; import java.util.List; import org.qortal.data.at.ATData; @@ -24,33 +25,38 @@ public class HSQLDBATRepository implements ATRepository { @Override public ATData fromATAddress(String atAddress) throws DataException { - final String sql = "SELECT creator, creation, version, asset_id, code_bytes, is_sleeping, sleep_until_height, is_finished, had_fatal_error, is_frozen, frozen_balance FROM ATs WHERE AT_address = ?"; + String sql = "SELECT creator, creation, version, asset_id, code_bytes, code_hash, " + + "is_sleeping, sleep_until_height, is_finished, had_fatal_error, " + + "is_frozen, frozen_balance " + + "FROM ATs " + + "WHERE AT_address = ? LIMIT 1"; try (ResultSet resultSet = this.repository.checkedExecute(sql, atAddress)) { if (resultSet == null) return null; byte[] creatorPublicKey = resultSet.getBytes(1); - long creation = resultSet.getTimestamp(2, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + long creation = getZonedTimestampMilli(resultSet, 2); int version = resultSet.getInt(3); long assetId = resultSet.getLong(4); byte[] codeBytes = resultSet.getBytes(5); // Actually BLOB - boolean isSleeping = resultSet.getBoolean(6); + byte[] codeHash = resultSet.getBytes(6); + boolean isSleeping = resultSet.getBoolean(7); - Integer sleepUntilHeight = resultSet.getInt(7); + Integer sleepUntilHeight = resultSet.getInt(8); if (sleepUntilHeight == 0 && resultSet.wasNull()) sleepUntilHeight = null; - boolean isFinished = resultSet.getBoolean(8); - boolean hadFatalError = resultSet.getBoolean(9); - boolean isFrozen = resultSet.getBoolean(10); + boolean isFinished = resultSet.getBoolean(9); + boolean hadFatalError = resultSet.getBoolean(10); + boolean isFrozen = resultSet.getBoolean(11); Long frozenBalance = resultSet.getLong(11); if (frozenBalance == 0 && resultSet.wasNull()) frozenBalance = null; - return new ATData(atAddress, creatorPublicKey, creation, version, assetId, codeBytes, isSleeping, sleepUntilHeight, isFinished, hadFatalError, - isFrozen, frozenBalance); + return new ATData(atAddress, creatorPublicKey, creation, version, assetId, codeBytes, codeHash, + isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance); } catch (SQLException e) { throw new DataException("Unable to fetch AT from repository", e); } @@ -67,9 +73,14 @@ public class HSQLDBATRepository implements ATRepository { @Override public List getAllExecutableATs() throws DataException { - final String sql = "SELECT AT_address, creator, creation, version, asset_id, code_bytes, is_sleeping, sleep_until_height, had_fatal_error, is_frozen, frozen_balance FROM ATs WHERE is_finished = false ORDER BY creation ASC"; + String sql = "SELECT AT_address, creator, creation, version, asset_id, code_bytes, code_hash, " + + "is_sleeping, sleep_until_height, had_fatal_error, " + + "is_frozen, frozen_balance " + + "FROM ATs " + + "WHERE is_finished = false " + + "ORDER BY creation ASC"; - List executableATs = new ArrayList(); + List executableATs = new ArrayList<>(); try (ResultSet resultSet = this.repository.checkedExecute(sql)) { if (resultSet == null) @@ -80,25 +91,26 @@ public class HSQLDBATRepository implements ATRepository { do { String atAddress = resultSet.getString(1); byte[] creatorPublicKey = resultSet.getBytes(2); - long creation = resultSet.getTimestamp(3, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + long creation = getZonedTimestampMilli(resultSet, 3); int version = resultSet.getInt(4); long assetId = resultSet.getLong(5); byte[] codeBytes = resultSet.getBytes(6); // Actually BLOB - boolean isSleeping = resultSet.getBoolean(7); + byte[] codeHash = resultSet.getBytes(7); + boolean isSleeping = resultSet.getBoolean(8); - Integer sleepUntilHeight = resultSet.getInt(8); + Integer sleepUntilHeight = resultSet.getInt(9); if (sleepUntilHeight == 0 && resultSet.wasNull()) sleepUntilHeight = null; - boolean hadFatalError = resultSet.getBoolean(9); - boolean isFrozen = resultSet.getBoolean(10); + boolean hadFatalError = resultSet.getBoolean(10); + boolean isFrozen = resultSet.getBoolean(11); Long frozenBalance = resultSet.getLong(11); if (frozenBalance == 0 && resultSet.wasNull()) frozenBalance = null; - ATData atData = new ATData(atAddress, creatorPublicKey, creation, version, assetId, codeBytes, isSleeping, sleepUntilHeight, isFinished, - hadFatalError, isFrozen, frozenBalance); + ATData atData = new ATData(atAddress, creatorPublicKey, creation, version, assetId, codeBytes, codeHash, + isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance); executableATs.add(atData); } while (resultSet.next()); @@ -109,9 +121,72 @@ public class HSQLDBATRepository implements ATRepository { } } + @Override + public List getATsByFunctionality(byte[] codeHash, Boolean isExecutable, Integer limit, Integer offset, Boolean reverse) throws DataException { + StringBuilder sql = new StringBuilder(512); + sql.append("SELECT AT_address, creator, creation, version, asset_id, code_bytes, ") + .append("is_sleeping, sleep_until_height, is_finished, had_fatal_error, ") + .append("is_frozen, frozen_balance ") + .append("FROM ATs ") + .append("WHERE code_hash = ? "); + + if (isExecutable != null) + sql.append("AND is_finished = ").append(isExecutable ? "false" : "true"); + + sql.append(" ORDER BY creation "); + if (reverse != null && reverse) + sql.append("DESC"); + + HSQLDBRepository.limitOffsetSql(sql, limit, offset); + + List matchingATs = new ArrayList<>(); + + try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), codeHash)) { + if (resultSet == null) + return matchingATs; + + do { + String atAddress = resultSet.getString(1); + byte[] creatorPublicKey = resultSet.getBytes(2); + long creation = getZonedTimestampMilli(resultSet, 3); + int version = resultSet.getInt(4); + long assetId = resultSet.getLong(5); + byte[] codeBytes = resultSet.getBytes(6); // Actually BLOB + boolean isSleeping = resultSet.getBoolean(7); + + Integer sleepUntilHeight = resultSet.getInt(8); + if (sleepUntilHeight == 0 && resultSet.wasNull()) + sleepUntilHeight = null; + + boolean isFinished = resultSet.getBoolean(9); + + boolean hadFatalError = resultSet.getBoolean(10); + boolean isFrozen = resultSet.getBoolean(11); + + Long frozenBalance = resultSet.getLong(12); + if (frozenBalance == 0 && resultSet.wasNull()) + frozenBalance = null; + + ATData atData = new ATData(atAddress, creatorPublicKey, creation, version, assetId, codeBytes, codeHash, + isSleeping, sleepUntilHeight, isFinished, hadFatalError, isFrozen, frozenBalance); + + matchingATs.add(atData); + } while (resultSet.next()); + + return matchingATs; + } catch (SQLException e) { + throw new DataException("Unable to fetch matching ATs from repository", e); + } + } + @Override public Integer getATCreationBlockHeight(String atAddress) throws DataException { - final String sql = "SELECT height from DeployATTransactions JOIN BlockTransactions ON transaction_signature = signature JOIN Blocks ON Blocks.signature = block_signature WHERE AT_address = ?"; + String sql = "SELECT height " + + "FROM DeployATTransactions " + + "JOIN BlockTransactions ON transaction_signature = signature " + + "JOIN Blocks ON Blocks.signature = block_signature " + + "WHERE AT_address = ? " + + "LIMIT 1"; try (ResultSet resultSet = this.repository.checkedExecute(sql, atAddress)) { if (resultSet == null) @@ -127,8 +202,9 @@ public class HSQLDBATRepository implements ATRepository { public void save(ATData atData) throws DataException { HSQLDBSaver saveHelper = new HSQLDBSaver("ATs"); - saveHelper.bind("AT_address", atData.getATAddress()).bind("creator", atData.getCreatorPublicKey()).bind("creation", new Timestamp(atData.getCreation())) - .bind("version", atData.getVersion()).bind("asset_id", atData.getAssetId()).bind("code_bytes", atData.getCodeBytes()) + saveHelper.bind("AT_address", atData.getATAddress()).bind("creator", atData.getCreatorPublicKey()).bind("creation", toOffsetDateTime(atData.getCreation())) + .bind("version", atData.getVersion()).bind("asset_id", atData.getAssetId()) + .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()); @@ -154,17 +230,22 @@ public class HSQLDBATRepository implements ATRepository { @Override public ATStateData getATStateAtHeight(String atAddress, int height) throws DataException { - try (ResultSet resultSet = this.repository - .checkedExecute("SELECT creation, state_data, state_hash, fees FROM ATStates WHERE AT_address = ? AND height = ?", atAddress, height)) { + String sql = "SELECT creation, state_data, state_hash, fees, is_initial " + + "FROM ATStates " + + "WHERE AT_address = ? AND height = ? " + + "LIMIT 1"; + + try (ResultSet resultSet = this.repository.checkedExecute(sql, atAddress, height)) { if (resultSet == null) return null; - long creation = resultSet.getTimestamp(1, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + long creation = getZonedTimestampMilli(resultSet, 1); byte[] stateData = resultSet.getBytes(2); // Actually BLOB byte[] stateHash = resultSet.getBytes(3); long fees = resultSet.getLong(4); + boolean isInitial = resultSet.getBoolean(5); - return new ATStateData(atAddress, height, creation, stateData, stateHash, fees); + return new ATStateData(atAddress, height, creation, stateData, stateHash, fees, isInitial); } catch (SQLException e) { throw new DataException("Unable to fetch AT state from repository", e); } @@ -172,18 +253,24 @@ public class HSQLDBATRepository implements ATRepository { @Override public ATStateData getLatestATState(String atAddress) throws DataException { - try (ResultSet resultSet = this.repository - .checkedExecute("SELECT height, creation, state_data, state_hash, fees FROM ATStates WHERE AT_address = ? ORDER BY height DESC", atAddress)) { + String sql = "SELECT height, creation, state_data, state_hash, fees, is_initial " + + "FROM ATStates " + + "WHERE AT_address = ? " + + "ORDER BY height DESC " + + "LIMIT 1"; + + try (ResultSet resultSet = this.repository.checkedExecute(sql, atAddress)) { if (resultSet == null) return null; int height = resultSet.getInt(1); - long creation = resultSet.getTimestamp(2, Calendar.getInstance(HSQLDBRepository.UTC)).getTime(); + long creation = getZonedTimestampMilli(resultSet, 2); byte[] stateData = resultSet.getBytes(3); // Actually BLOB byte[] stateHash = resultSet.getBytes(4); long fees = resultSet.getLong(5); + boolean isInitial = resultSet.getBoolean(6); - return new ATStateData(atAddress, height, creation, stateData, stateHash, fees); + return new ATStateData(atAddress, height, creation, stateData, stateHash, fees, isInitial); } catch (SQLException e) { throw new DataException("Unable to fetch latest AT state from repository", e); } @@ -191,10 +278,14 @@ public class HSQLDBATRepository implements ATRepository { @Override public List getBlockATStatesAtHeight(int height) throws DataException { + String sql = "SELECT AT_address, state_hash, fees, is_initial " + + "FROM ATStates " + + "WHERE height = ? " + + "ORDER BY creation ASC"; + List atStates = new ArrayList<>(); - try (ResultSet resultSet = this.repository.checkedExecute("SELECT AT_address, state_hash, fees FROM ATStates WHERE height = ? ORDER BY creation ASC", - height)) { + try (ResultSet resultSet = this.repository.checkedExecute(sql, height)) { if (resultSet == null) return atStates; // No atStates in this block @@ -203,8 +294,9 @@ public class HSQLDBATRepository implements ATRepository { String atAddress = resultSet.getString(1); byte[] stateHash = resultSet.getBytes(2); long fees = resultSet.getLong(3); + boolean isInitial = resultSet.getBoolean(4); - ATStateData atStateData = new ATStateData(atAddress, height, stateHash, fees); + ATStateData atStateData = new ATStateData(atAddress, height, stateHash, fees, isInitial); atStates.add(atStateData); } while (resultSet.next()); } catch (SQLException e) { @@ -223,8 +315,9 @@ public class HSQLDBATRepository implements ATRepository { HSQLDBSaver saveHelper = new HSQLDBSaver("ATStates"); saveHelper.bind("AT_address", atStateData.getATAddress()).bind("height", atStateData.getHeight()) - .bind("creation", new Timestamp(atStateData.getCreation())).bind("state_data", atStateData.getStateData()) - .bind("state_hash", atStateData.getStateHash()).bind("fees", atStateData.getFees()); + .bind("creation", toOffsetDateTime(atStateData.getCreation())).bind("state_data", atStateData.getStateData()) + .bind("state_hash", atStateData.getStateHash()).bind("fees", atStateData.getFees()) + .bind("is_initial", atStateData.isInitial()); try { saveHelper.execute(this.repository); diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java index 012b5ce4..80de6926 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -976,6 +976,16 @@ public class HSQLDBDatabaseUpdates { stmt.execute("CHECKPOINT DEFRAG"); break; + case 71: + // Add flag to AT state data to indicate 'initial deployment state' + stmt.execute("ALTER TABLE ATStates ADD COLUMN is_initial BOOLEAN NOT NULL DEFAULT TRUE"); + break; + + case 72: + // For ATs, add hash of code bytes to allow searching for specific function ATs, e.g. cross-chain trading + stmt.execute("ALTER TABLE ATs ADD COLUMN code_hash VARBINARY(32) NOT NULL BEFORE is_sleeping"); // Assuming something like SHA256 + break; + default: // nothing to do return false; diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java index 83d46d8b..7ed761cd 100644 --- a/src/main/java/org/qortal/settings/Settings.java +++ b/src/main/java/org/qortal/settings/Settings.java @@ -20,6 +20,7 @@ import org.eclipse.persistence.exceptions.XMLMarshalException; import org.eclipse.persistence.jaxb.JAXBContextFactory; import org.eclipse.persistence.jaxb.UnmarshallerProperties; import org.qortal.block.BlockChain; +import org.qortal.crosschain.BTC.BitcoinNet; // All properties to be converted to JSON via JAXB @XmlAccessorType(XmlAccessType.FIELD) @@ -91,7 +92,7 @@ public class Settings { // Which blockchains this node is running private String blockchainConfig = null; // use default from resources - private boolean useBitcoinTestNet = false; + private BitcoinNet bitcoinNet = BitcoinNet.MAIN; // Repository related /** Queries that take longer than this are logged. (milliseconds) */ @@ -345,8 +346,8 @@ public class Settings { return this.blockchainConfig; } - public boolean useBitcoinTestNet() { - return this.useBitcoinTestNet; + public BitcoinNet getBitcoinNet() { + return this.bitcoinNet; } public Long getSlowQueryThreshold() { diff --git a/src/main/java/org/qortal/transaction/AtTransaction.java b/src/main/java/org/qortal/transaction/AtTransaction.java index f463a0c4..2c086de3 100644 --- a/src/main/java/org/qortal/transaction/AtTransaction.java +++ b/src/main/java/org/qortal/transaction/AtTransaction.java @@ -145,6 +145,8 @@ public class AtTransaction extends Transaction { @Override public void processReferencesAndFees() throws DataException { + getATAccount().setLastReference(this.atTransactionData.getSignature()); + if (this.atTransactionData.getAmount() != null) { Account recipient = getRecipient(); long assetId = this.atTransactionData.getAssetId(); @@ -152,7 +154,7 @@ public class AtTransaction extends Transaction { // For QORT amounts only: if recipient has no reference yet, then this is their starting reference if (assetId == Asset.QORT && recipient.getLastReference() == null) // In Qora1 last reference was set to 64-bytes of zero - // In Qortal we use AT-Transction's signature, which makes more sense + // In Qortal we use AT-Transaction's signature, which makes more sense recipient.setLastReference(this.atTransactionData.getSignature()); } } @@ -179,6 +181,8 @@ public class AtTransaction extends Transaction { @Override public void orphanReferencesAndFees() throws DataException { + getATAccount().setLastReference(this.atTransactionData.getReference()); + if (this.atTransactionData.getAmount() != null) { Account recipient = getRecipient(); diff --git a/src/main/java/org/qortal/transaction/DeployAtTransaction.java b/src/main/java/org/qortal/transaction/DeployAtTransaction.java index 0a055d3f..96333b55 100644 --- a/src/main/java/org/qortal/transaction/DeployAtTransaction.java +++ b/src/main/java/org/qortal/transaction/DeployAtTransaction.java @@ -1,23 +1,25 @@ package org.qortal.transaction; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; import org.ciyam.at.MachineState; +import org.ciyam.at.Timestamp; import org.qortal.account.Account; import org.qortal.asset.Asset; import org.qortal.at.AT; +import org.qortal.at.QortalATAPI; +import org.qortal.at.QortalAtLoggerFactory; import org.qortal.crypto.Crypto; import org.qortal.data.asset.AssetData; +import org.qortal.data.at.ATData; import org.qortal.data.transaction.DeployAtTransactionData; import org.qortal.data.transaction.TransactionData; import org.qortal.repository.DataException; import org.qortal.repository.Repository; -import org.qortal.transform.Transformer; +import org.qortal.transform.TransformationException; import org.qortal.utils.Amounts; +import org.qortal.transform.transaction.TransactionTransformer; import com.google.common.base.Utf8; @@ -51,7 +53,7 @@ public class DeployAtTransaction extends Transaction { /** Returns AT version from the header bytes */ private short getVersion() { byte[] creationBytes = deployATTransactionData.getCreationBytes(); - return (short) ((creationBytes[0] & 0xff) | (creationBytes[1] << 8)); // Little-endian + return (short) ((creationBytes[0] << 8) | (creationBytes[1] & 0xff)); // Big-endian } /** Make sure deployATTransactionData has an ATAddress */ @@ -59,29 +61,14 @@ public class DeployAtTransaction extends Transaction { if (this.deployATTransactionData.getAtAddress() != null) return; - int blockHeight = this.getHeight(); - if (blockHeight == 0) - blockHeight = this.repository.getBlockRepository().getBlockchainHeight() + 1; - - byte[] name = this.deployATTransactionData.getName().getBytes(StandardCharsets.UTF_8); - byte[] description = this.deployATTransactionData.getDescription().replaceAll("\\s", "").getBytes(StandardCharsets.UTF_8); - byte[] creatorPublicKey = this.deployATTransactionData.getCreatorPublicKey(); - byte[] creationBytes = this.deployATTransactionData.getCreationBytes(); - - ByteBuffer byteBuffer = ByteBuffer - .allocate(name.length + description.length + creatorPublicKey.length + creationBytes.length + Transformer.INT_LENGTH); - - byteBuffer.order(ByteOrder.LITTLE_ENDIAN); - - byteBuffer.put(name); - byteBuffer.put(description); - byteBuffer.put(creatorPublicKey); - byteBuffer.put(creationBytes); - byteBuffer.putInt(blockHeight); - - String atAddress = Crypto.toATAddress(byteBuffer.array()); - - this.deployATTransactionData.setAtAddress(atAddress); + // Use transaction transformer + try { + String atAddress = Crypto.toATAddress(TransactionTransformer.toBytesForSigning(this.deployATTransactionData)); + this.deployATTransactionData.setAtAddress(atAddress); + return; + } catch (TransformationException e) { + throw new DataException("Unable to generate AT address"); + } } // Navigation @@ -154,8 +141,22 @@ public class DeployAtTransaction extends Transaction { // Check creation bytes are valid (for v2+) if (this.getVersion() >= 2) { // Do actual validation + ensureATAddress(); + + // Just enough AT data to allow API to query initial balances, etc. + String atAddress = this.deployATTransactionData.getAtAddress(); + byte[] creatorPublicKey = this.deployATTransactionData.getCreatorPublicKey(); + long creation = this.deployATTransactionData.getTimestamp(); + ATData skeletonAtData = new ATData(atAddress, creatorPublicKey, creation, assetId); + + int height = this.repository.getBlockRepository().getBlockchainHeight() + 1; + long blockTimestamp = Timestamp.toLong(height, 0); + + QortalATAPI api = new QortalATAPI(repository, skeletonAtData, blockTimestamp); + QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance(); + try { - new MachineState(this.deployATTransactionData.getCreationBytes()); + new MachineState(api, loggerFactory, this.deployATTransactionData.getCreationBytes()); } catch (IllegalArgumentException e) { // Not valid return ValidationResult.INVALID_CREATION_BYTES; diff --git a/src/main/java/org/qortal/transaction/Transaction.java b/src/main/java/org/qortal/transaction/Transaction.java index 35c0076e..9d457dbd 100644 --- a/src/main/java/org/qortal/transaction/Transaction.java +++ b/src/main/java/org/qortal/transaction/Transaction.java @@ -980,11 +980,16 @@ public abstract class Transaction { // AT transactions come before non-AT transactions if (td1.getType() == TransactionType.AT && td2.getType() != TransactionType.AT) return -1; + // Non-AT transactions come after AT transactions if (td1.getType() != TransactionType.AT && td2.getType() == TransactionType.AT) return 1; - // Both transactions are either AT or non-AT so compare timestamps + // If both transactions are AT type, then preserve existing ordering. + if (td1.getType() == TransactionType.AT) + return 0; + + // Both transactions are non-AT so compare timestamps int result = Long.compare(td1.getTimestamp(), td2.getTimestamp()); if (result == 0) diff --git a/src/main/java/org/qortal/transform/block/BlockTransformer.java b/src/main/java/org/qortal/transform/block/BlockTransformer.java index d4c14925..3a529ba5 100644 --- a/src/main/java/org/qortal/transform/block/BlockTransformer.java +++ b/src/main/java/org/qortal/transform/block/BlockTransformer.java @@ -138,6 +138,9 @@ public class BlockTransformer extends Transformer { atStates.add(new ATStateData(atAddress, stateHash, fees)); } + // Bump byteBuffer over AT states just read in slice + byteBuffer.position(byteBuffer.position() + atBytesLength); + // AT count to reflect the number of states we have atCount = atStates.size(); @@ -264,6 +267,10 @@ public class BlockTransformer extends Transformer { bytes.write(Ints.toByteArray(atBytesLength)); for (ATStateData atStateData : block.getATStates()) { + // Skip initial states generated by DEPLOY_AT transactions in the same block + if (atStateData.isInitial()) + continue; + bytes.write(Base58.decode(atStateData.getATAddress())); bytes.write(atStateData.getStateHash()); bytes.write(Longs.toByteArray(atStateData.getFees())); @@ -273,6 +280,10 @@ public class BlockTransformer extends Transformer { bytes.write(Ints.toByteArray(blockData.getTransactionCount())); for (Transaction transaction : block.getTransactions()) { + // Don't serialize AT transactions! + if (transaction.getTransactionData().getType() == TransactionType.AT) + continue; + TransactionData transactionData = transaction.getTransactionData(); bytes.write(Ints.toByteArray(TransactionTransformer.getDataLength(transactionData))); bytes.write(TransactionTransformer.toBytes(transactionData)); diff --git a/src/main/java/org/qortal/utils/BitTwiddling.java b/src/main/java/org/qortal/utils/BitTwiddling.java index e17e6034..ada2c2f5 100644 --- a/src/main/java/org/qortal/utils/BitTwiddling.java +++ b/src/main/java/org/qortal/utils/BitTwiddling.java @@ -21,4 +21,9 @@ public class BitTwiddling { return maxValue; } + /** Convert int to little-endian byte array */ + public static byte[] toLEByteArray(int value) { + return new byte[] { (byte) (value), (byte) (value >> 8), (byte) (value >> 16), (byte) (value >> 24) }; + } + } diff --git a/src/main/resources/blockchain.json b/src/main/resources/blockchain.json index fba5f4ba..9ff887b0 100644 --- a/src/main/resources/blockchain.json +++ b/src/main/resources/blockchain.json @@ -40,6 +40,12 @@ "blockTimingsByHeight": [ { "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 } ], + "ciyamAtSettings": { + "feePerStep": "0.0001", + "maxStepsPerRound": 500, + "stepsPerFunctionCall": 10, + "minutesPerBlock": 1 + }, "featureTriggers": { "messageHeight": 0, "atHeight": 0, diff --git a/src/test/java/org/qortal/test/apps/BTCACCTTests.java b/src/test/java/org/qortal/test/apps/BTCACCTTests.java index e33eacfa..499cf743 100644 --- a/src/test/java/org/qortal/test/apps/BTCACCTTests.java +++ b/src/test/java/org/qortal/test/apps/BTCACCTTests.java @@ -70,11 +70,11 @@ public class BTCACCTTests { // For when we want to re-run private static final byte[] prevSecret = HashCode.fromString("30a13291e350214bea5318f990b77bc11d2cb709f7c39859f248bef396961dcc").asBytes(); private static final long prevLockTime = 1539347892L; - private static final boolean usePreviousFundingTx = true; + private static final boolean usePreviousFundingTx = false; private static final boolean doRefundNotRedeem = false; - public void main(String[] args) throws NoSuchAlgorithmException, InsufficientMoneyException, InterruptedException, ExecutionException, UnknownHostException { + public static void main(String[] args) throws NoSuchAlgorithmException, InsufficientMoneyException, InterruptedException, ExecutionException, UnknownHostException { Security.insertProviderAt(new BouncyCastleProvider(), 0); byte[] secret = new byte[32]; @@ -173,7 +173,7 @@ public class BTCACCTTests { private static final byte[] redeemScript4 = HashCode.fromString("b17576a914").asBytes(); private static final byte[] redeemScript5 = HashCode.fromString("88ac68").asBytes(); - private byte[] buildRedeemScript(byte[] secret, byte[] senderPubKey, byte[] recipientPubKey, long lockTime) { + private static byte[] buildRedeemScript(byte[] secret, byte[] senderPubKey, byte[] recipientPubKey, long lockTime) { try { MessageDigest sha256Digester = MessageDigest.getInstance("SHA-256"); @@ -188,7 +188,7 @@ public class BTCACCTTests { } } - private byte[] hash160(byte[] input) { + private static byte[] hash160(byte[] input) { try { MessageDigest rmd160Digester = MessageDigest.getInstance("RIPEMD160"); MessageDigest sha256Digester = MessageDigest.getInstance("SHA-256"); @@ -199,7 +199,7 @@ public class BTCACCTTests { } } - private Transaction buildFundingTransaction(NetworkParameters params, Sha256Hash prevTxHash, long outputIndex, Coin balance, ECKey sigKey, Coin value, + private static Transaction buildFundingTransaction(NetworkParameters params, Sha256Hash prevTxHash, long outputIndex, Coin balance, ECKey sigKey, Coin value, byte[] redeemScriptHash) { Transaction fundingTransaction = new Transaction(params); @@ -218,7 +218,7 @@ public class BTCACCTTests { return fundingTransaction; } - private Transaction buildRedeemTransaction(NetworkParameters params, TransactionOutPoint fundingOutPoint, ECKey recipientKey, Coin value, byte[] secret, + private static Transaction buildRedeemTransaction(NetworkParameters params, TransactionOutPoint fundingOutPoint, ECKey recipientKey, Coin value, byte[] secret, byte[] redeemScriptBytes) { Transaction redeemTransaction = new Transaction(params); redeemTransaction.setVersion(2); @@ -255,7 +255,7 @@ public class BTCACCTTests { return redeemTransaction; } - private Transaction buildRefundTransaction(NetworkParameters params, TransactionOutPoint fundingOutPoint, ECKey senderKey, Coin value, + private static Transaction buildRefundTransaction(NetworkParameters params, TransactionOutPoint fundingOutPoint, ECKey senderKey, Coin value, byte[] redeemScriptBytes, long lockTime) { Transaction refundTransaction = new Transaction(params); refundTransaction.setVersion(2); @@ -294,7 +294,7 @@ public class BTCACCTTests { return refundTransaction; } - private void broadcastWithConfirmation(WalletAppKit kit, Transaction transaction) { + private static void broadcastWithConfirmation(WalletAppKit kit, Transaction transaction) { System.out.println("Broadcasting tx: " + transaction.getTxId().toString()); System.out.println("TX hex: " + HashCode.fromBytes(transaction.bitcoinSerialize()).toString()); @@ -320,7 +320,7 @@ public class BTCACCTTests { } /** Convert int to little-endian byte array */ - private byte[] toLEByteArray(int value) { + private static byte[] toLEByteArray(int value) { return new byte[] { (byte) (value), (byte) (value >> 8), (byte) (value >> 16), (byte) (value >> 24) }; } diff --git a/src/test/java/org/qortal/test/apps/BuildCheckpoints.java b/src/test/java/org/qortal/test/apps/BuildCheckpoints.java new file mode 100644 index 00000000..48cc570c --- /dev/null +++ b/src/test/java/org/qortal/test/apps/BuildCheckpoints.java @@ -0,0 +1,67 @@ +package org.qortal.test.apps; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.TreeMap; + +import org.bitcoinj.core.BlockChain; +import org.bitcoinj.core.CheckpointManager; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.PeerAddress; +import org.bitcoinj.core.PeerGroup; +import org.bitcoinj.core.StoredBlock; +import org.bitcoinj.params.RegTestParams; +import org.bitcoinj.store.BlockStore; +import org.bitcoinj.store.MemoryBlockStore; + +public class BuildCheckpoints { + + private static final TreeMap checkpoints = new TreeMap<>(); + + public static void main(String[] args) throws Exception { + final NetworkParameters params = RegTestParams.get(); + + final BlockStore store = new MemoryBlockStore(params); + final BlockChain chain = new BlockChain(params, store); + final PeerGroup peerGroup = new PeerGroup(params, chain); + + final InetAddress ipAddress = InetAddress.getLoopbackAddress(); + final PeerAddress peerAddress = new PeerAddress(params, ipAddress); + peerGroup.addAddress(peerAddress); + peerGroup.start(); + + chain.addNewBestBlockListener((block) -> checkpoints.put(block.getHeight(), block)); + + peerGroup.downloadBlockChain(); + peerGroup.stop(); + + final File checkpointsFile = new File("regtest-checkpoints"); + saveAsText(checkpointsFile); + } + + private static void saveAsText(File textFile) { + try (PrintWriter writer = new PrintWriter( + new OutputStreamWriter(new FileOutputStream(textFile), StandardCharsets.US_ASCII))) { + writer.println("TXT CHECKPOINTS 1"); + writer.println("0"); // Number of signatures to read. Do this later. + writer.println(checkpoints.size()); + + ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE); + + for (StoredBlock block : checkpoints.values()) { + block.serializeCompact(buffer); + writer.println(CheckpointManager.BASE64.encode(buffer.array())); + buffer.position(0); + } + } catch (FileNotFoundException e) { + return; + } + } + +} diff --git a/src/test/java/org/qortal/test/apps/orphan.java b/src/test/java/org/qortal/test/apps/orphan.java index 58853e55..f847f98d 100644 --- a/src/test/java/org/qortal/test/apps/orphan.java +++ b/src/test/java/org/qortal/test/apps/orphan.java @@ -13,17 +13,23 @@ import org.qortal.settings.Settings; public class orphan { public static void main(String[] args) { - if (args.length == 0) { - System.err.println("usage: orphan "); + if (args.length < 1 || args.length > 2) { + System.err.println("usage: orphan [] "); System.exit(1); } - int targetHeight = Integer.parseInt(args[0]); - Security.insertProviderAt(new BouncyCastleProvider(), 0); - // Load/check settings, which potentially sets up blockchain config, etc. - Settings.getInstance(); + int argIndex = 0; + + if (args.length > 1) { + Settings.fileInstance(args[argIndex++]); + } else { + // Load/check settings, which potentially sets up blockchain config, etc. + Settings.getInstance(); + } + + int targetHeight = Integer.parseInt(args[argIndex]); try { RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl()); diff --git a/src/test/java/org/qortal/test/btcacct/AtTests.java b/src/test/java/org/qortal/test/btcacct/AtTests.java new file mode 100644 index 00000000..1d4a6e0f --- /dev/null +++ b/src/test/java/org/qortal/test/btcacct/AtTests.java @@ -0,0 +1,552 @@ +package org.qortal.test.btcacct; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +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.function.Function; + +import org.bitcoinj.core.Base58; +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.crosschain.BTCACCT; +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 com.google.common.hash.HashCode; +import com.google.common.primitives.Bytes; + +public class AtTests extends Common { + + public static final byte[] secret = "This string is exactly 32 bytes!".getBytes(); + public static final byte[] secretHash = Crypto.hash160(secret); // daf59884b4d1aec8c1b17102530909ee43c0151a + public static final int refundTimeout = 10; // blocks + public static final long initialPayout = 100000L; + public static final long redeemAmount = 80_40200000L; + public static final long fundingAmount = 123_45600000L; + public static final long bitcoinAmount = 864200L; + + @Before + public void beforeTest() throws DataException { + Common.useDefaultSettings(); + } + + @Test + public void testCompile() { + Account deployer = Common.getTestAccount(null, "chloe"); + + byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), secretHash, refundTimeout, initialPayout, redeemAmount, bitcoinAmount); + System.out.println("CIYAM 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 recipient = Common.getTestAccount(repository, "dilbert"); + + long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); + long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT); + + DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer); + + 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 = recipientsInitialBalance; + actualBalance = recipient.getConfirmedBalance(Asset.QORT); + + assertEquals("Recipient'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 = recipientsInitialBalance; + actualBalance = recipient.getConfirmedBalance(Asset.QORT); + + assertEquals("Recipient'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 recipient = Common.getTestAccount(repository, "dilbert"); + + long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); + long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT); + + DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer); + Account at = deployAtTransaction.getATAccount(); + String atAddress = at.getAddress(); + + long deployAtFee = deployAtTransaction.getTransactionData().getFee(); + long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee; + + // Send creator's address to AT + byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(deployer.getAddress()), 32, 0); + MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress); + long messageFee = messageTransaction.getTransactionData().getFee(); + + // Refund should happen 1st block after receiving recipient address + BlockUtils.mintBlock(repository); + + 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); + + describeAt(repository, atAddress); + + // Test orphaning + BlockUtils.orphanLastBlock(repository); + + 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 testInitialPayment() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); + PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert"); + + long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); + long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT); + + DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer); + Account at = deployAtTransaction.getATAccount(); + String atAddress = at.getAddress(); + + // Send recipient's address to AT + byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0); + MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress); + + // Initial payment should happen 1st block after receiving recipient address + BlockUtils.mintBlock(repository); + + long expectedBalance = recipientsInitialBalance + initialPayout; + long actualBalance = recipient.getConfirmedBalance(Asset.QORT); + + assertEquals("Recipient's post-initial-payout balance incorrect", expectedBalance, actualBalance); + + describeAt(repository, atAddress); + + // Test orphaning + BlockUtils.orphanLastBlock(repository); + + expectedBalance = recipientsInitialBalance; + actualBalance = recipient.getConfirmedBalance(Asset.QORT); + + assertEquals("Recipient's pre-initial-payout balance incorrect", expectedBalance, actualBalance); + } + } + + // TEST SENDING RECIPIENT ADDRESS 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 recipient = Common.getTestAccount(repository, "dilbert"); + PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob"); + + long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); + long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT); + + DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer); + Account at = deployAtTransaction.getATAccount(); + String atAddress = at.getAddress(); + + // Send recipient's address to AT BUT NOT FROM AT CREATOR + byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0); + MessageTransaction messageTransaction = sendMessage(repository, bystander, recipientAddressBytes, atAddress); + + // Initial payment should NOT happen + BlockUtils.mintBlock(repository); + + long expectedBalance = recipientsInitialBalance; + long actualBalance = recipient.getConfirmedBalance(Asset.QORT); + + assertEquals("Recipient's post-initial-payout balance incorrect", expectedBalance, actualBalance); + + describeAt(repository, atAddress); + } + } + + @SuppressWarnings("unused") + @Test + public void testAutomaticTradeRefund() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); + PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert"); + + long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); + long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT); + + DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer); + Account at = deployAtTransaction.getATAccount(); + String atAddress = at.getAddress(); + + // Send recipient's address to AT + byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0); + MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress); + + // Initial payment should happen 1st block after receiving recipient address + BlockUtils.mintBlock(repository); + + long deployAtFee = deployAtTransaction.getTransactionData().getFee(); + long messageFee = messageTransaction.getTransactionData().getFee(); + long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee - messageFee; + + checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee); + + describeAt(repository, atAddress); + + // Test orphaning + BlockUtils.orphanLastBlock(repository); + BlockUtils.orphanLastBlock(repository); + + 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 recipient = Common.getTestAccount(repository, "dilbert"); + + long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); + long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT); + + DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer); + Account at = deployAtTransaction.getATAccount(); + String atAddress = at.getAddress(); + + // Send recipient's address to AT + byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0); + MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress); + + // Initial payment should happen 1st block after receiving recipient address + BlockUtils.mintBlock(repository); + + // Send correct secret to AT + messageTransaction = sendMessage(repository, recipient, secret, atAddress); + + // AT should send funds in the next block + ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress); + BlockUtils.mintBlock(repository); + + long expectedBalance = recipientsInitialBalance + initialPayout - messageTransaction.getTransactionData().getFee() + redeemAmount; + long actualBalance = recipient.getConfirmedBalance(Asset.QORT); + + assertEquals("Recipent's post-redeem balance incorrect", expectedBalance, actualBalance); + + describeAt(repository, atAddress); + + // Orphan redeem + BlockUtils.orphanLastBlock(repository); + + expectedBalance = recipientsInitialBalance + initialPayout - messageTransaction.getTransactionData().getFee(); + actualBalance = recipient.getConfirmedBalance(Asset.QORT); + + assertEquals("Recipent'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 recipient = Common.getTestAccount(repository, "dilbert"); + PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob"); + + long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); + long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT); + + DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer); + long deployAtFee = deployAtTransaction.getTransactionData().getFee(); + + Account at = deployAtTransaction.getATAccount(); + String atAddress = at.getAddress(); + + // Send recipient's address to AT + byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0); + MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress); + + // Initial payment should happen 1st block after receiving recipient address + BlockUtils.mintBlock(repository); + + // Send correct secret to AT, but from wrong account + messageTransaction = sendMessage(repository, bystander, secret, atAddress); + + // AT should NOT send funds in the next block + ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress); + BlockUtils.mintBlock(repository); + + long expectedBalance = recipientsInitialBalance + initialPayout; + long actualBalance = recipient.getConfirmedBalance(Asset.QORT); + + assertEquals("Recipent's balance incorrect", expectedBalance, actualBalance); + + describeAt(repository, atAddress); + + 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 recipient = Common.getTestAccount(repository, "dilbert"); + + long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); + long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT); + + DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer); + long deployAtFee = deployAtTransaction.getTransactionData().getFee(); + + Account at = deployAtTransaction.getATAccount(); + String atAddress = at.getAddress(); + + // Send recipient's address to AT + byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0); + MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress); + + // Initial payment should happen 1st block after receiving recipient address + BlockUtils.mintBlock(repository); + + // Send correct secret to AT, but from wrong account + byte[] wrongSecret = Crypto.digest(secret); + messageTransaction = sendMessage(repository, recipient, wrongSecret, atAddress); + + // AT should NOT send funds in the next block + ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress); + BlockUtils.mintBlock(repository); + + long expectedBalance = recipientsInitialBalance + initialPayout - messageTransaction.getTransactionData().getFee(); + long actualBalance = recipient.getConfirmedBalance(Asset.QORT); + + assertEquals("Recipent's balance incorrect", expectedBalance, actualBalance); + + describeAt(repository, atAddress); + + checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee); + } + } + + @SuppressWarnings("unused") + @Test + public void testDescribeDeployed() throws DataException { + try (final Repository repository = RepositoryManager.getRepository()) { + PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); + PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert"); + + long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); + long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT); + + DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer); + + List 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, BTCACCT.CODE_BYTES_HASH)) + continue; + + describeAt(repository, atAddress); + } + } + } + + private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer) throws DataException { + byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), secretHash, refundTimeout, initialPayout, redeemAmount, bitcoinAmount); + + 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-BTC cross-chain trade"; + String description = String.format("Qortal-Bitcoin cross-chain trade"); + String atType = "ACCT"; + String tags = "QORT-BTC 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; + long amount = 0; + + BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null); + TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, 4, recipient, Asset.QORT, amount, 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; + + // 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 - initialPayout; + + 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 = BTCACCT.populateTradeData(repository, atData); + + Function 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" + + "\tcreator: %s,\n" + + "\tcreation timestamp: %s,\n" + + "\tcurrent balance: %s QORT,\n" + + "\tHASH160 of secret: %s,\n" + + "\tinitial payout: %s QORT,\n" + + "\tredeem payout: %s QORT,\n" + + "\texpected bitcoin: %s BTC,\n" + + "\ttrade timeout: %d minutes (from trade start),\n" + + "\tcurrent block height: %d,\n", + tradeData.qortalAtAddress, + tradeData.qortalCreator, + epochMilliFormatter.apply(tradeData.creationTimestamp), + Amounts.prettyAmount(tradeData.qortBalance), + HashCode.fromBytes(tradeData.secretHash).toString().substring(0, 40), + Amounts.prettyAmount(tradeData.initialPayout), + Amounts.prettyAmount(tradeData.redeemPayout), + Amounts.prettyAmount(tradeData.expectedBitcoin), + tradeData.tradeRefundTimeout, + currentBlockHeight)); + + // Are we in 'offer' or 'trade' stage? + if (tradeData.tradeRefundHeight == null) { + // Offer + System.out.println(String.format("\tstatus: 'offer mode'")); + } else { + // Trade + System.out.println(String.format("\tstatus: 'trade mode',\n" + + "\ttrade timeout: block %d,\n" + + "\tBitcoin P2SH nLockTime: %d (%s),\n" + + "\ttrade recipient: %s", + tradeData.tradeRefundHeight, + tradeData.lockTime, epochMilliFormatter.apply(tradeData.lockTime * 1000L), + tradeData.qortalRecipient)); + } + } + +} diff --git a/src/test/java/org/qortal/test/btcacct/BtcTests.java b/src/test/java/org/qortal/test/btcacct/BtcTests.java new file mode 100644 index 00000000..8a25b937 --- /dev/null +++ b/src/test/java/org/qortal/test/btcacct/BtcTests.java @@ -0,0 +1,65 @@ +package org.qortal.test.btcacct; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.bitcoinj.store.BlockStoreException; +import org.bitcoinj.wallet.WalletTransaction; +import org.junit.Before; +import org.junit.Test; +import org.qortal.crosschain.BTC; +import org.qortal.crosschain.BTCACCT; +import org.qortal.repository.DataException; +import org.qortal.test.common.Common; + +public class BtcTests extends Common { + + @Before + public void beforeTest() throws DataException { + Common.useDefaultSettings(); + } + + @Test + public void testGetMedianBlockTime() throws BlockStoreException { + System.out.println(String.format("Starting BTC instance...")); + BTC btc = BTC.getInstance(); + System.out.println(String.format("BTC instance started")); + + long before = System.currentTimeMillis(); + System.out.println(String.format("Bitcoin median blocktime: %d", btc.getMedianBlockTime())); + long afterFirst = System.currentTimeMillis(); + + System.out.println(String.format("Bitcoin median blocktime: %d", btc.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 + public void testFindP2shSecret() { + // This actually exists on TEST3 + String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE"; + int startTime = 1587510000; + + List walletTransactions = new ArrayList<>(); + + BTC.getInstance().getBalanceAndOtherInfo(p2shAddress, startTime, null, walletTransactions); + + byte[] expectedSecret = AtTests.secret; + byte[] secret = BTCACCT.findP2shSecret(p2shAddress, walletTransactions); + + assertNotNull(secret); + assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret)); + } + +} diff --git a/src/test/java/org/qortal/test/btcacct/BuildP2SH.java b/src/test/java/org/qortal/test/btcacct/BuildP2SH.java new file mode 100644 index 00000000..33f86526 --- /dev/null +++ b/src/test/java/org/qortal/test/btcacct/BuildP2SH.java @@ -0,0 +1,125 @@ +package org.qortal.test.btcacct; + +import java.security.Security; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.LegacyAddress; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.script.Script.ScriptType; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.qortal.controller.Controller; +import org.qortal.crosschain.BTC; +import org.qortal.crosschain.BTCACCT; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryFactory; +import org.qortal.repository.RepositoryManager; +import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory; +import org.qortal.settings.Settings; + +import com.google.common.hash.HashCode; + +public class BuildP2SH { + + private static void usage(String error) { + if (error != null) + System.err.println(error); + + System.err.println(String.format("usage: BuildP2SH ()")); + System.err.println(String.format("example: BuildP2SH " + + "mrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n" + + "\t0.00008642 \\\n" + + "\tn2N5VKrzq39nmuefZwp3wBiF4icdXX2B6o \\\n" + + "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n" + + "\t1585920000")); + System.exit(1); + } + + public static void main(String[] args) { + if (args.length < 5 || args.length > 6) + usage(null); + + Security.insertProviderAt(new BouncyCastleProvider(), 0); + Settings.fileInstance("settings-test.json"); + + BTC btc = BTC.getInstance(); + NetworkParameters params = btc.getNetworkParameters(); + + Address refundBitcoinAddress = null; + Coin bitcoinAmount = null; + Address redeemBitcoinAddress = null; + byte[] secretHash = null; + int lockTime = 0; + Coin bitcoinFee = Common.DEFAULT_BTC_FEE; + + int argIndex = 0; + try { + refundBitcoinAddress = Address.fromString(params, args[argIndex++]); + if (refundBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH) + usage("Refund BTC address must be in P2PKH form"); + + bitcoinAmount = Coin.parseCoin(args[argIndex++]); + + redeemBitcoinAddress = Address.fromString(params, args[argIndex++]); + if (redeemBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH) + usage("Redeem BTC address must be in P2PKH form"); + + secretHash = HashCode.fromString(args[argIndex++]).asBytes(); + if (secretHash.length != 20) + usage("Hash of secret must be 20 bytes"); + + lockTime = Integer.parseInt(args[argIndex++]); + int refundTimeoutDelay = lockTime - (int) (System.currentTimeMillis() / 1000L); + if (refundTimeoutDelay < 600 || refundTimeoutDelay > 30 * 24 * 60 * 60) + usage("Locktime (seconds) should be at between 10 minutes and 1 month from now"); + + if (args.length > argIndex) + bitcoinFee = Coin.parseCoin(args[argIndex++]); + } catch (IllegalArgumentException e) { + usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); + } + + try { + RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl()); + RepositoryManager.setRepositoryFactory(repositoryFactory); + } catch (DataException e) { + throw new RuntimeException("Repository startup issue: " + e.getMessage()); + } + + try (final Repository repository = RepositoryManager.getRepository()) { + System.out.println("Confirm the following is correct based on the info you've given:"); + + System.out.println(String.format("Refund Bitcoin address: %s", refundBitcoinAddress)); + System.out.println(String.format("Bitcoin redeem amount: %s", bitcoinAmount.toPlainString())); + + System.out.println(String.format("Redeem Bitcoin address: %s", redeemBitcoinAddress)); + System.out.println(String.format("Redeem miner's fee: %s", BTC.FORMAT.format(bitcoinFee))); + + System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime)); + System.out.println(String.format("Hash of secret: %s", HashCode.fromBytes(secretHash))); + + byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash); + System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes))); + + byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes); + + Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); + System.out.println(String.format("P2SH address: %s", p2shAddress)); + + bitcoinAmount = bitcoinAmount.add(bitcoinFee); + + // Fund P2SH + System.out.println(String.format("\nYou need to fund %s with %s (includes redeem/refund fee of %s)", + p2shAddress.toString(), BTC.FORMAT.format(bitcoinAmount), BTC.FORMAT.format(bitcoinFee))); + + System.out.println("Once this is done, responder should run Respond to check P2SH funding and create AT"); + } catch (DataException e) { + throw new RuntimeException("Repository issue: " + e.getMessage()); + } + } + +} diff --git a/src/test/java/org/qortal/test/btcacct/CheckP2SH.java b/src/test/java/org/qortal/test/btcacct/CheckP2SH.java new file mode 100644 index 00000000..ec2ccb86 --- /dev/null +++ b/src/test/java/org/qortal/test/btcacct/CheckP2SH.java @@ -0,0 +1,172 @@ +package org.qortal.test.btcacct; + +import java.security.Security; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.List; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.LegacyAddress; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.TransactionOutput; +import org.bitcoinj.script.Script.ScriptType; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.qortal.controller.Controller; +import org.qortal.crosschain.BTC; +import org.qortal.crosschain.BTCACCT; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryFactory; +import org.qortal.repository.RepositoryManager; +import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory; +import org.qortal.settings.Settings; + +import com.google.common.hash.HashCode; + +public class CheckP2SH { + + private static void usage(String error) { + if (error != null) + System.err.println(error); + + System.err.println(String.format("usage: CheckP2SH ()")); + System.err.println(String.format("example: CheckP2SH " + + "2NEZboTLhBDPPQciR7sExBhy3TsDi7wV3Cv \\\n" + + "mrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n" + + "\t0.00008642 \\\n" + + "\tn2N5VKrzq39nmuefZwp3wBiF4icdXX2B6o \\\n" + + "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n" + + "\t1585920000")); + System.exit(1); + } + + public static void main(String[] args) { + if (args.length < 6 || args.length > 7) + usage(null); + + Security.insertProviderAt(new BouncyCastleProvider(), 0); + Settings.fileInstance("settings-test.json"); + + BTC btc = BTC.getInstance(); + NetworkParameters params = btc.getNetworkParameters(); + + Address p2shAddress = null; + Address refundBitcoinAddress = null; + Coin bitcoinAmount = null; + Address redeemBitcoinAddress = null; + byte[] secretHash = null; + int lockTime = 0; + Coin bitcoinFee = Common.DEFAULT_BTC_FEE; + + int argIndex = 0; + try { + p2shAddress = Address.fromString(params, args[argIndex++]); + if (p2shAddress.getOutputScriptType() != ScriptType.P2SH) + usage("P2SH address invalid"); + + refundBitcoinAddress = Address.fromString(params, args[argIndex++]); + if (refundBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH) + usage("Refund BTC address must be in P2PKH form"); + + bitcoinAmount = Coin.parseCoin(args[argIndex++]); + + redeemBitcoinAddress = Address.fromString(params, args[argIndex++]); + if (redeemBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH) + usage("Redeem BTC address must be in P2PKH form"); + + secretHash = HashCode.fromString(args[argIndex++]).asBytes(); + if (secretHash.length != 20) + usage("Hash of secret must be 20 bytes"); + + lockTime = Integer.parseInt(args[argIndex++]); + int refundTimeoutDelay = lockTime - (int) (System.currentTimeMillis() / 1000L); + if (refundTimeoutDelay < 600 || refundTimeoutDelay > 7 * 24 * 60 * 60) + usage("Locktime (seconds) should be at between 10 minutes and 1 week from now"); + + if (args.length > argIndex) + bitcoinFee = Coin.parseCoin(args[argIndex++]); + } catch (IllegalArgumentException e) { + usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); + } + + try { + RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl()); + RepositoryManager.setRepositoryFactory(repositoryFactory); + } catch (DataException e) { + throw new RuntimeException("Repository startup issue: " + e.getMessage()); + } + + try (final Repository repository = RepositoryManager.getRepository()) { + System.out.println("Confirm the following is correct based on the info you've given:"); + + System.out.println(String.format("Refund Bitcoin address: %s", redeemBitcoinAddress)); + System.out.println(String.format("Bitcoin redeem amount: %s", bitcoinAmount.toPlainString())); + + System.out.println(String.format("Redeem Bitcoin address: %s", refundBitcoinAddress)); + System.out.println(String.format("Redeem miner's fee: %s", BTC.FORMAT.format(bitcoinFee))); + + System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime)); + System.out.println(String.format("Hash of secret: %s", HashCode.fromBytes(secretHash))); + + System.out.println(String.format("P2SH address: %s", p2shAddress)); + + byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash); + System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes))); + + byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes); + Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); + + if (!derivedP2shAddress.equals(p2shAddress)) { + System.err.println(String.format("Derived P2SH address %s does not match given address %s", derivedP2shAddress, p2shAddress)); + System.exit(2); + } + + bitcoinAmount = bitcoinAmount.add(bitcoinFee); + + long medianBlockTime = BTC.getInstance().getMedianBlockTime(); + System.out.println(String.format("Median block time: %s", LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC))); + + long now = System.currentTimeMillis(); + + if (now < medianBlockTime * 1000L) + System.out.println(String.format("Too soon (%s) to redeem based on median block time %s", LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.UTC), LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC))); + + // Check P2SH is funded + final int startTime = lockTime - 86400; + + Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString(), startTime); + if (p2shBalance == null) { + System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress)); + System.exit(2); + } + System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, BTC.FORMAT.format(p2shBalance))); + + // Grab all P2SH funding transactions (just in case there are more than one) + List fundingOutputs = BTC.getInstance().getOutputs(p2shAddress.toString(), startTime); + if (fundingOutputs == null) { + System.err.println(String.format("Can't find outputs for P2SH")); + System.exit(2); + } + + System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : ""))); + + for (TransactionOutput fundingOutput : fundingOutputs) + System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), BTC.FORMAT.format(fundingOutput.getValue()))); + + if (fundingOutputs.isEmpty()) { + System.err.println(String.format("Can't redeem spent/unfunded P2SH")); + System.exit(2); + } + + if (fundingOutputs.size() != 1) { + System.err.println(String.format("Expecting only one unspent output for P2SH")); + System.exit(2); + } + } catch (DataException e) { + throw new RuntimeException("Repository issue: " + e.getMessage()); + } + } + +} diff --git a/src/test/java/org/qortal/test/btcacct/Common.java b/src/test/java/org/qortal/test/btcacct/Common.java new file mode 100644 index 00000000..320d1c1c --- /dev/null +++ b/src/test/java/org/qortal/test/btcacct/Common.java @@ -0,0 +1,9 @@ +package org.qortal.test.btcacct; + +import org.bitcoinj.core.Coin; + +public abstract class Common { + + public static final Coin DEFAULT_BTC_FEE = Coin.parseCoin("0.00001000"); + +} diff --git a/src/test/java/org/qortal/test/btcacct/DeployAT.java b/src/test/java/org/qortal/test/btcacct/DeployAT.java new file mode 100644 index 00000000..98672164 --- /dev/null +++ b/src/test/java/org/qortal/test/btcacct/DeployAT.java @@ -0,0 +1,160 @@ +package org.qortal.test.btcacct; + +import java.security.Security; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.qortal.account.PrivateKeyAccount; +import org.qortal.asset.Asset; +import org.qortal.controller.Controller; +import org.qortal.crosschain.BTCACCT; +import org.qortal.data.transaction.BaseTransactionData; +import org.qortal.data.transaction.DeployAtTransactionData; +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.RepositoryFactory; +import org.qortal.repository.RepositoryManager; +import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory; +import org.qortal.settings.Settings; +import org.qortal.transaction.DeployAtTransaction; +import org.qortal.transaction.Transaction; +import org.qortal.transform.TransformationException; +import org.qortal.transform.transaction.TransactionTransformer; +import org.qortal.utils.Amounts; +import org.qortal.utils.Base58; + +import com.google.common.hash.HashCode; + +public class DeployAT { + + public static final long atFundingExtra = 2000000L; + + private static void usage(String error) { + if (error != null) + System.err.println(error); + + System.err.println(String.format("usage: DeployAT ")); + System.err.println(String.format("example: DeployAT " + + "AdTd9SUEYSdTW8mgK3Gu72K97bCHGdUwi2VvLNjUohot \\\n" + + "\t80.4020 \\\n" + + "\t0.00864200 \\\n" + + "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n" + + "\t0.0001 \\\n" + + "\t123.456 \\\n" + + "\t10")); + System.exit(1); + } + + public static void main(String[] args) { + if (args.length != 8) + usage(null); + + Security.insertProviderAt(new BouncyCastleProvider(), 0); + Settings.fileInstance("settings-test.json"); + + byte[] refundPrivateKey = null; + long redeemAmount = 0; + long expectedBitcoin = 0; + byte[] secretHash = null; + long initialPayout = 0; + long fundingAmount = 0; + int tradeTimeout = 0; + + int argIndex = 0; + try { + refundPrivateKey = Base58.decode(args[argIndex++]); + if (refundPrivateKey.length != 32) + usage("Refund private key must be 32 bytes"); + + redeemAmount = Long.parseLong(args[argIndex++]); + if (redeemAmount <= 0) + usage("QORT amount must be positive"); + + expectedBitcoin = Long.parseLong(args[argIndex++]); + if (expectedBitcoin <= 0) + usage("Expected BTC amount must be positive"); + + secretHash = HashCode.fromString(args[argIndex++]).asBytes(); + if (secretHash.length != 20) + usage("Hash of secret must be 20 bytes"); + + initialPayout = Long.parseLong(args[argIndex++]); + if (initialPayout < 0) + usage("Initial QORT payout must be positive"); + + fundingAmount = Long.parseLong(args[argIndex++]); + if (fundingAmount <= redeemAmount) + usage("AT funding amount must be greater than QORT redeem amount"); + + tradeTimeout = Integer.parseInt(args[argIndex++]); + if (tradeTimeout < 10 || tradeTimeout > 50000) + usage("AT trade timeout should be between 10 and 50,000 minutes"); + } catch (IllegalArgumentException e) { + usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); + } + + try { + RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl()); + RepositoryManager.setRepositoryFactory(repositoryFactory); + } catch (DataException e) { + throw new RuntimeException("Repository startup issue: " + e.getMessage()); + } + + try (final Repository repository = RepositoryManager.getRepository()) { + System.out.println("Confirm the following is correct based on the info you've given:"); + + PrivateKeyAccount refundAccount = new PrivateKeyAccount(repository, refundPrivateKey); + System.out.println(String.format("Refund Qortal address: %s", refundAccount.getAddress())); + + System.out.println(String.format("QORT redeem amount: %s", Amounts.prettyAmount(redeemAmount))); + + System.out.println(String.format("AT funding amount: %s", Amounts.prettyAmount(fundingAmount))); + + System.out.println(String.format("HASH160 of secret: %s", HashCode.fromBytes(secretHash))); + + // Deploy AT + byte[] creationBytes = BTCACCT.buildQortalAT(refundAccount.getAddress(), secretHash, tradeTimeout, initialPayout, redeemAmount, expectedBitcoin); + System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString()); + + long txTimestamp = System.currentTimeMillis(); + byte[] lastReference = refundAccount.getLastReference(); + + if (lastReference == null) { + System.err.println(String.format("Qortal account %s has no last reference", refundAccount.getAddress())); + System.exit(2); + } + + Long fee = null; + String name = "QORT-BTC cross-chain trade"; + String description = String.format("Qortal-Bitcoin cross-chain trade"); + String atType = "ACCT"; + String tags = "QORT-BTC ACCT"; + + BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, refundAccount.getPublicKey(), fee, null); + TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT); + + Transaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData); + + fee = deployAtTransaction.calcRecommendedFee(); + deployAtTransactionData.setFee(fee); + + deployAtTransaction.sign(refundAccount); + + byte[] signedBytes = null; + try { + signedBytes = TransactionTransformer.toBytes(deployAtTransactionData); + } catch (TransformationException e) { + System.err.println(String.format("Unable to convert transaction to base58: %s", e.getMessage())); + System.exit(2); + } + + System.out.println(String.format("\nSigned transaction in base58, ready for POST /transactions/process:\n%s\n", Base58.encode(signedBytes))); + } catch (NumberFormatException e) { + usage(String.format("Number format exception: %s", e.getMessage())); + } catch (DataException e) { + throw new RuntimeException("Repository issue: " + e.getMessage()); + } + } + +} diff --git a/src/test/java/org/qortal/test/btcacct/GetTransaction.java b/src/test/java/org/qortal/test/btcacct/GetTransaction.java new file mode 100644 index 00000000..48af4ebf --- /dev/null +++ b/src/test/java/org/qortal/test/btcacct/GetTransaction.java @@ -0,0 +1,64 @@ +package org.qortal.test.btcacct; + +import java.security.Security; +import java.util.List; + +import org.bitcoinj.core.AddressFormatException; +import org.bitcoinj.core.TransactionOutput; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.qortal.crosschain.BTC; +import org.qortal.settings.Settings; + +import com.google.common.hash.HashCode; + +public class GetTransaction { + + static { + // This must go before any calls to LogManager/Logger + System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager"); + } + + private static void usage(String error) { + if (error != null) + System.err.println(error); + + System.err.println(String.format("usage: GetTransaction ")); + System.err.println(String.format("example (mainnet): GetTransaction 816407e79568f165f13e09e9912c5f2243e0a23a007cec425acedc2e89284660 1585317000")); + System.err.println(String.format("example (testnet): GetTransaction 3bfd17a492a4e3d6cb7204e17e20aca6c1ab82e1828bd1106eefbaf086fb8a4e 1584376000")); + System.exit(1); + } + + public static void main(String[] args) { + if (args.length < 2 || args.length > 2) + usage(null); + + Security.insertProviderAt(new BouncyCastleProvider(), 0); + Settings.fileInstance("settings-test.json"); + + byte[] transactionId = null; + int startTime = 0; + + try { + int argIndex = 0; + + transactionId = HashCode.fromString(args[argIndex++]).asBytes(); + + startTime = Integer.parseInt(args[argIndex++]); + } catch (NumberFormatException | AddressFormatException e) { + usage(String.format("Argument format exception: %s", e.getMessage())); + } + + // Grab all outputs from transaction + List fundingOutputs = BTC.getInstance().getOutputs(transactionId, startTime); + if (fundingOutputs == null) { + System.out.println(String.format("Transaction not found")); + return; + } + + System.out.println(String.format("Found %d output%s", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : ""))); + + for (TransactionOutput fundingOutput : fundingOutputs) + System.out.println(String.format("Output %d: %s", fundingOutput.getIndex(), fundingOutput.getValue().toPlainString())); + } + +} diff --git a/src/test/java/org/qortal/test/btcacct/Redeem.java b/src/test/java/org/qortal/test/btcacct/Redeem.java new file mode 100644 index 00000000..4c5b9fb7 --- /dev/null +++ b/src/test/java/org/qortal/test/btcacct/Redeem.java @@ -0,0 +1,198 @@ +package org.qortal.test.btcacct; + +import java.security.Security; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.Arrays; +import java.util.List; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.LegacyAddress; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionOutput; +import org.bitcoinj.script.Script.ScriptType; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.qortal.controller.Controller; +import org.qortal.crosschain.BTC; +import org.qortal.crosschain.BTCACCT; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryFactory; +import org.qortal.repository.RepositoryManager; +import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory; +import org.qortal.settings.Settings; + +import com.google.common.hash.HashCode; + +public class Redeem { + + static { + // This must go before any calls to LogManager/Logger + System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager"); + } + + private static void usage(String error) { + if (error != null) + System.err.println(error); + + System.err.println(String.format("usage: Redeem ()")); + System.err.println(String.format("example: Redeem " + + "2NEZboTLhBDPPQciR7sExBhy3TsDi7wV3Cv \\\n" + + "\tmrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n" + + "\tec199a4abc9d3bf024349e397535dfee9d287e174aeabae94237eb03a0118c03 \\\n" + + "\t5468697320737472696e672069732065786163746c7920333220627974657321 \\\n" + + "\t1585920000")); + System.exit(1); + } + + public static void main(String[] args) { + if (args.length < 5 || args.length > 6) + usage(null); + + Security.insertProviderAt(new BouncyCastleProvider(), 0); + Settings.fileInstance("settings-test.json"); + + BTC btc = BTC.getInstance(); + NetworkParameters params = btc.getNetworkParameters(); + + Address p2shAddress = null; + Address refundBitcoinAddress = null; + byte[] redeemPrivateKey = null; + byte[] secret = null; + int lockTime = 0; + Coin bitcoinFee = Common.DEFAULT_BTC_FEE; + + int argIndex = 0; + try { + p2shAddress = Address.fromString(params, args[argIndex++]); + if (p2shAddress.getOutputScriptType() != ScriptType.P2SH) + usage("P2SH address invalid"); + + refundBitcoinAddress = Address.fromString(params, args[argIndex++]); + if (refundBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH) + usage("Refund BTC address must be in P2PKH form"); + + redeemPrivateKey = HashCode.fromString(args[argIndex++]).asBytes(); + // Auto-trim + if (redeemPrivateKey.length >= 37 && redeemPrivateKey.length <= 38) + redeemPrivateKey = Arrays.copyOfRange(redeemPrivateKey, 1, 33); + if (redeemPrivateKey.length != 32) + usage("Redeem private key must be 32 bytes"); + + secret = HashCode.fromString(args[argIndex++]).asBytes(); + if (secret.length == 0) + usage("Invalid secret bytes"); + + lockTime = Integer.parseInt(args[argIndex++]); + + if (args.length > argIndex) + bitcoinFee = Coin.parseCoin(args[argIndex++]); + } catch (IllegalArgumentException e) { + usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); + } + + try { + RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl()); + RepositoryManager.setRepositoryFactory(repositoryFactory); + } catch (DataException e) { + throw new RuntimeException("Repository startup issue: " + e.getMessage()); + } + + try (final Repository repository = RepositoryManager.getRepository()) { + System.out.println("Confirm the following is correct based on the info you've given:"); + + System.out.println(String.format("Redeem PRIVATE key: %s", HashCode.fromBytes(redeemPrivateKey))); + System.out.println(String.format("Redeem miner's fee: %s", BTC.FORMAT.format(bitcoinFee))); + System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime)); + + // New/derived info + + byte[] secretHash = BTC.hash160(secret); + System.out.println(String.format("HASH160 of secret: %s", HashCode.fromBytes(secretHash))); + + ECKey redeemKey = ECKey.fromPrivate(redeemPrivateKey); + Address redeemAddress = Address.fromKey(params, redeemKey, ScriptType.P2PKH); + System.out.println(String.format("Redeem recipient (PKH): %s (%s)", redeemAddress, HashCode.fromBytes(redeemAddress.getHash()))); + + System.out.println(String.format("P2SH address: %s", p2shAddress)); + + byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash); + System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes))); + + byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes); + Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); + + if (!derivedP2shAddress.equals(p2shAddress)) { + System.err.println(String.format("Derived P2SH address %s does not match given address %s", derivedP2shAddress, p2shAddress)); + System.exit(2); + } + + // Some checks + + System.out.println("\nProcessing:"); + + long medianBlockTime = BTC.getInstance().getMedianBlockTime(); + System.out.println(String.format("Median block time: %s", LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC))); + + long now = System.currentTimeMillis(); + + if (now < medianBlockTime * 1000L) { + System.err.println(String.format("Too soon (%s) to redeem based on median block time %s", LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.UTC), LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC))); + System.exit(2); + } + + // Check P2SH is funded + final int startTime = ((int) (System.currentTimeMillis() / 1000L)) - 86400; + + Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString(), startTime); + if (p2shBalance == null) { + System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress)); + System.exit(2); + } + System.out.println(String.format("P2SH address %s balance: %s BTC", p2shAddress, p2shBalance.toPlainString())); + + // Grab all P2SH funding transactions (just in case there are more than one) + List fundingOutputs = BTC.getInstance().getOutputs(p2shAddress.toString(), startTime); + if (fundingOutputs == null) { + System.err.println(String.format("Can't find outputs for P2SH")); + System.exit(2); + } + + System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : ""))); + + for (TransactionOutput fundingOutput : fundingOutputs) + System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), BTC.FORMAT.format(fundingOutput.getValue()))); + + if (fundingOutputs.isEmpty()) { + System.err.println(String.format("Can't redeem spent/unfunded P2SH")); + System.exit(2); + } + + if (fundingOutputs.size() != 1) { + System.err.println(String.format("Expecting only one unspent output for P2SH")); + System.exit(2); + } + + TransactionOutput fundingOutput = fundingOutputs.get(0); + System.out.println(String.format("Using output %s:%d for redeem", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex())); + + Coin redeemAmount = p2shBalance.subtract(bitcoinFee); + System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.FORMAT.format(redeemAmount), BTC.FORMAT.format(bitcoinFee))); + + Transaction redeemTransaction = BTCACCT.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutput, redeemScriptBytes, secret); + + byte[] redeemBytes = redeemTransaction.bitcoinSerialize(); + + System.out.println(String.format("\nLoad this transaction into your wallet and broadcast:\n%s\n", HashCode.fromBytes(redeemBytes).toString())); + } catch (NumberFormatException e) { + usage(String.format("Number format exception: %s", e.getMessage())); + } catch (DataException e) { + throw new RuntimeException("Repository issue: " + e.getMessage()); + } + } + +} diff --git a/src/test/java/org/qortal/test/btcacct/Refund.java b/src/test/java/org/qortal/test/btcacct/Refund.java new file mode 100644 index 00000000..3393f8bb --- /dev/null +++ b/src/test/java/org/qortal/test/btcacct/Refund.java @@ -0,0 +1,202 @@ +package org.qortal.test.btcacct; + +import java.security.Security; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.Arrays; +import java.util.List; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.LegacyAddress; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionOutput; +import org.bitcoinj.script.Script.ScriptType; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.qortal.controller.Controller; +import org.qortal.crosschain.BTC; +import org.qortal.crosschain.BTCACCT; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryFactory; +import org.qortal.repository.RepositoryManager; +import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory; +import org.qortal.settings.Settings; + +import com.google.common.hash.HashCode; + +public class Refund { + + static { + // This must go before any calls to LogManager/Logger + System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager"); + } + + private static void usage(String error) { + if (error != null) + System.err.println(error); + + System.err.println(String.format("usage: Refund ()")); + System.err.println(String.format("example: Refund " + + "2NEZboTLhBDPPQciR7sExBhy3TsDi7wV3Cv \\\n" + + "\tef027fb5828c5e201eaf6de4cd3b0b340d16a191ef848cd691f35ef8f727358c9c01b576fb7e \\\n" + + "\tn2N5VKrzq39nmuefZwp3wBiF4icdXX2B6o \\\n" + + "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n" + + "\t1585920000")); + System.exit(1); + } + + public static void main(String[] args) { + if (args.length < 5 || args.length > 6) + usage(null); + + Security.insertProviderAt(new BouncyCastleProvider(), 0); + Settings.fileInstance("settings-test.json"); + + BTC btc = BTC.getInstance(); + NetworkParameters params = btc.getNetworkParameters(); + + Address p2shAddress = null; + byte[] refundPrivateKey = null; + Address redeemBitcoinAddress = null; + byte[] secretHash = null; + int lockTime = 0; + Coin bitcoinFee = Common.DEFAULT_BTC_FEE; + + int argIndex = 0; + try { + p2shAddress = Address.fromString(params, args[argIndex++]); + if (p2shAddress.getOutputScriptType() != ScriptType.P2SH) + usage("P2SH address invalid"); + + refundPrivateKey = HashCode.fromString(args[argIndex++]).asBytes(); + // Auto-trim + if (refundPrivateKey.length >= 37 && refundPrivateKey.length <= 38) + refundPrivateKey = Arrays.copyOfRange(refundPrivateKey, 1, 33); + if (refundPrivateKey.length != 32) + usage("Refund private key must be 32 bytes"); + + redeemBitcoinAddress = Address.fromString(params, args[argIndex++]); + if (redeemBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH) + usage("Their BTC address must be in P2PKH form"); + + secretHash = HashCode.fromString(args[argIndex++]).asBytes(); + if (secretHash.length != 20) + usage("HASH160 of secret must be 20 bytes"); + + lockTime = Integer.parseInt(args[argIndex++]); + + if (args.length > argIndex) + bitcoinFee = Coin.parseCoin(args[argIndex++]); + } catch (IllegalArgumentException e) { + usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); + } + + try { + RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl()); + RepositoryManager.setRepositoryFactory(repositoryFactory); + } catch (DataException e) { + throw new RuntimeException("Repository startup issue: " + e.getMessage()); + } + + try (final Repository repository = RepositoryManager.getRepository()) { + System.out.println("Confirm the following is correct based on the info you've given:"); + + System.out.println(String.format("Refund PRIVATE key: %s", HashCode.fromBytes(refundPrivateKey))); + System.out.println(String.format("Redeem Bitcoin address: %s", redeemBitcoinAddress)); + System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime)); + System.out.println(String.format("P2SH address: %s", p2shAddress)); + System.out.println(String.format("Refund miner's fee: %s", BTC.FORMAT.format(bitcoinFee))); + + // New/derived info + + System.out.println("\nCHECKING info from other party:"); + + ECKey refundKey = ECKey.fromPrivate(refundPrivateKey); + Address refundAddress = Address.fromKey(params, refundKey, ScriptType.P2PKH); + System.out.println(String.format("Refund recipient (PKH): %s (%s)", refundAddress, HashCode.fromBytes(refundAddress.getHash()))); + + byte[] redeemScriptBytes = BTCACCT.buildScript(refundAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash); + System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes))); + + byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes); + Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); + + if (!derivedP2shAddress.equals(p2shAddress)) { + System.err.println(String.format("Derived P2SH address %s does not match given address %s", derivedP2shAddress, p2shAddress)); + System.exit(2); + } + + // Some checks + + System.out.println("\nProcessing:"); + + long medianBlockTime = BTC.getInstance().getMedianBlockTime(); + System.out.println(String.format("Median block time: %s", LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC))); + + long now = System.currentTimeMillis(); + + if (now < medianBlockTime * 1000L) { + System.err.println(String.format("Too soon (%s) to refund based on median block time %s", LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.UTC), LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC))); + System.exit(2); + } + + if (now < lockTime * 1000L) { + System.err.println(String.format("Too soon (%s) to refund based on lockTime %s", LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.UTC), LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC))); + System.exit(2); + } + + // Check P2SH is funded + final int startTime = ((int) (System.currentTimeMillis() / 1000L)) - 86400; + + Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString(), startTime); + if (p2shBalance == null) { + System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress)); + System.exit(2); + } + System.out.println(String.format("P2SH address %s balance: %s BTC", p2shAddress, p2shBalance.toPlainString())); + + // Grab all P2SH funding transactions (just in case there are more than one) + List fundingOutputs = BTC.getInstance().getOutputs(p2shAddress.toString(), startTime); + if (fundingOutputs == null) { + System.err.println(String.format("Can't find outputs for P2SH")); + System.exit(2); + } + + System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : ""))); + + for (TransactionOutput fundingOutput : fundingOutputs) + System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), BTC.FORMAT.format(fundingOutput.getValue()))); + + if (fundingOutputs.isEmpty()) { + System.err.println(String.format("Can't refund spent/unfunded P2SH")); + System.exit(2); + } + + if (fundingOutputs.size() != 1) { + System.err.println(String.format("Expecting only one unspent output for P2SH")); + System.exit(2); + } + + TransactionOutput fundingOutput = fundingOutputs.get(0); + System.out.println(String.format("Using output %s:%d for refund", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex())); + + Coin refundAmount = p2shBalance.subtract(bitcoinFee); + System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.FORMAT.format(refundAmount), BTC.FORMAT.format(bitcoinFee))); + + Transaction redeemTransaction = BTCACCT.buildRefundTransaction(refundAmount, refundKey, fundingOutput, redeemScriptBytes, lockTime); + + byte[] redeemBytes = redeemTransaction.bitcoinSerialize(); + + System.out.println(String.format("\nLoad this transaction into your wallet and broadcast:\n%s\n", HashCode.fromBytes(redeemBytes).toString())); + } catch (NumberFormatException e) { + usage(String.format("Number format exception: %s", e.getMessage())); + } catch (DataException e) { + throw new RuntimeException("Repository issue: " + e.getMessage()); + } + } + +} diff --git a/src/test/java/org/qortal/test/common/BlockUtils.java b/src/test/java/org/qortal/test/common/BlockUtils.java index c85f6d01..82c41d50 100644 --- a/src/test/java/org/qortal/test/common/BlockUtils.java +++ b/src/test/java/org/qortal/test/common/BlockUtils.java @@ -15,9 +15,9 @@ public class BlockUtils { private static final Logger LOGGER = LogManager.getLogger(BlockUtils.class); /** Mints a new block using "alice-reward-share" test account. */ - public static void mintBlock(Repository repository) throws DataException { + public static Block mintBlock(Repository repository) throws DataException { PrivateKeyAccount mintingAccount = Common.getTestAccount(repository, "alice-reward-share"); - BlockMinter.mintTestingBlock(repository, mintingAccount); + return BlockMinter.mintTestingBlock(repository, mintingAccount); } public static Long getNextBlockReward(Repository repository) throws DataException { diff --git a/src/test/resources/test-chain-v2-founder-rewards.json b/src/test/resources/test-chain-v2-founder-rewards.json index 4e57d6ff..18adf859 100644 --- a/src/test/resources/test-chain-v2-founder-rewards.json +++ b/src/test/resources/test-chain-v2-founder-rewards.json @@ -29,6 +29,12 @@ "blockTimingsByHeight": [ { "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 } ], + "ciyamAtSettings": { + "feePerStep": "0.0001", + "maxStepsPerRound": 500, + "stepsPerFunctionCall": 10, + "minutesPerBlock": 1 + }, "featureTriggers": { "messageHeight": 0, "atHeight": 0, diff --git a/src/test/resources/test-chain-v2-minting.json b/src/test/resources/test-chain-v2-minting.json index 0d85e4fe..90b5a306 100644 --- a/src/test/resources/test-chain-v2-minting.json +++ b/src/test/resources/test-chain-v2-minting.json @@ -29,6 +29,12 @@ "blockTimingsByHeight": [ { "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 } ], + "ciyamAtSettings": { + "feePerStep": "0.0001", + "maxStepsPerRound": 500, + "stepsPerFunctionCall": 10, + "minutesPerBlock": 1 + }, "featureTriggers": { "messageHeight": 0, "atHeight": 0, diff --git a/src/test/resources/test-chain-v2-qora-holder.json b/src/test/resources/test-chain-v2-qora-holder.json index b540655f..59503f91 100644 --- a/src/test/resources/test-chain-v2-qora-holder.json +++ b/src/test/resources/test-chain-v2-qora-holder.json @@ -29,6 +29,12 @@ "blockTimingsByHeight": [ { "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 } ], + "ciyamAtSettings": { + "feePerStep": "0.0001", + "maxStepsPerRound": 500, + "stepsPerFunctionCall": 10, + "minutesPerBlock": 1 + }, "featureTriggers": { "messageHeight": 0, "atHeight": 0, diff --git a/src/test/resources/test-chain-v2.json b/src/test/resources/test-chain-v2.json index 7696f225..bb5d1afd 100644 --- a/src/test/resources/test-chain-v2.json +++ b/src/test/resources/test-chain-v2.json @@ -29,6 +29,12 @@ "blockTimingsByHeight": [ { "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 } ], + "ciyamAtSettings": { + "feePerStep": "0.0001", + "maxStepsPerRound": 500, + "stepsPerFunctionCall": 10, + "minutesPerBlock": 1 + }, "featureTriggers": { "messageHeight": 0, "atHeight": 0, diff --git a/src/test/resources/test-settings-v2-founder-rewards.json b/src/test/resources/test-settings-v2-founder-rewards.json index 1751cf7d..c89df187 100644 --- a/src/test/resources/test-settings-v2-founder-rewards.json +++ b/src/test/resources/test-settings-v2-founder-rewards.json @@ -2,5 +2,6 @@ "restrictedApi": false, "blockchainConfig": "src/test/resources/test-chain-v2-founder-rewards.json", "wipeUnconfirmedOnStart": false, + "testNtpOffset": 0, "minPeers": 0 } diff --git a/src/test/resources/test-settings-v2-minting.json b/src/test/resources/test-settings-v2-minting.json index eba9a655..9c72c375 100644 --- a/src/test/resources/test-settings-v2-minting.json +++ b/src/test/resources/test-settings-v2-minting.json @@ -2,5 +2,6 @@ "restrictedApi": false, "blockchainConfig": "src/test/resources/test-chain-v2-minting.json", "wipeUnconfirmedOnStart": false, + "testNtpOffset": 0, "minPeers": 0 } diff --git a/src/test/resources/test-settings-v2-qora-holder.json b/src/test/resources/test-settings-v2-qora-holder.json index 0248c867..83b23287 100644 --- a/src/test/resources/test-settings-v2-qora-holder.json +++ b/src/test/resources/test-settings-v2-qora-holder.json @@ -2,5 +2,6 @@ "restrictedApi": false, "blockchainConfig": "src/test/resources/test-chain-v2-qora-holder.json", "wipeUnconfirmedOnStart": false, + "testNtpOffset": 0, "minPeers": 0 } diff --git a/src/test/resources/test-settings-v2.json b/src/test/resources/test-settings-v2.json index 57eb22a5..1cefddee 100644 --- a/src/test/resources/test-settings-v2.json +++ b/src/test/resources/test-settings-v2.json @@ -1,4 +1,5 @@ { + "bitcoinNet": "TEST3", "restrictedApi": false, "blockchainConfig": "src/test/resources/test-chain-v2.json", "wipeUnconfirmedOnStart": false,