More work on cross-chain trading, including API calls.

Added API calls to aid Qortal-side of cross-chain trading.
POST /crosschain/build - for building Qortal AT
POST /crosschain/tradeoffer/recipient - for sending trade partner/recipient to AT
POST /crosschain/tradeoffer/secret - for sending secret to AT
DELETE /crosschain/tradeoffer - for cancelling AT

More fixes regarding Blocks processing/orphaning ATs.
More fixes regarding sending/receiving blocks containing AT data.
AT-related fix to genesis block.

Improved cross-chain trading AT code, removing offer-mode timeout
and replacing that with allowing AT creator to cancel offer/end AT
by sending AT the creator's own address as trade partner/recipient.
After all, they're not going to trade with themselves.

Added assertion to check BTCACCT.CODE_BYTES_HASH matches compiled code hash.

Added cross-chain AT's 'mode' for easier diagnosis, either OFFER or TRADE.

We can't use AT's signature to generate AT address because address is needed
before DEPLOY_AT transaction is signed. So we use a hash of signature-less
transaction bytes.

Corresponding changes to tests.
This commit is contained in:
catbref
2020-04-21 09:31:09 +01:00
parent 8baf42765e
commit 94d18538d8
19 changed files with 663 additions and 127 deletions

View File

@@ -61,7 +61,7 @@ public class AtTests extends Common {
public void testCompile() {
Account deployer = Common.getTestAccount(null, "chloe");
byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), secretHash, refundTimeout, refundTimeout, initialPayout, redeemAmount, bitcoinAmount);
byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), secretHash, refundTimeout, initialPayout, redeemAmount, bitcoinAmount);
System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
}
@@ -113,7 +113,7 @@ public class AtTests extends Common {
@SuppressWarnings("unused")
@Test
public void testAutomaticOfferRefund() throws DataException {
public void testOfferCancel() throws DataException {
try (final Repository repository = RepositoryManager.getRepository()) {
PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert");
@@ -128,15 +128,29 @@ public class AtTests extends Common {
BigDecimal deployAtFee = deployAtTransaction.getTransactionData().getFee();
BigDecimal deployersPostDeploymentBalance = deployersInitialBalance.subtract(fundingAmount).subtract(deployAtFee);
checkAtRefund(repository, deployer, deployersInitialBalance, deployAtFee);
// Send creator's address to AT
byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(deployer.getAddress()), 32, 0);
MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress);
BigDecimal messageFee = messageTransaction.getTransactionData().getFee();
// Refund should happen 1st block after receiving recipient address
BlockUtils.mintBlock(repository);
BigDecimal expectedMinimumBalance = deployersPostDeploymentBalance;
BigDecimal expectedMaximumBalance = deployersInitialBalance.subtract(deployAtFee).subtract(messageFee);
BigDecimal actualBalance = deployer.getConfirmedBalance(Asset.QORT);
assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance.toPlainString(), expectedMinimumBalance.toPlainString()), actualBalance.compareTo(expectedMinimumBalance) > 0);
assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance.toPlainString(), expectedMaximumBalance.toPlainString()), actualBalance.compareTo(expectedMaximumBalance) < 0);
describeAt(repository, atAddress);
// Test orphaning
BlockUtils.orphanLastBlock(repository);
BigDecimal expectedBalance = deployersPostDeploymentBalance;
BigDecimal actualBalance = deployer.getBalance(Asset.QORT);
BigDecimal expectedBalance = deployersPostDeploymentBalance.subtract(messageFee);
actualBalance = deployer.getBalance(Asset.QORT);
Common.assertEqualBigDecimals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
}
@@ -237,7 +251,7 @@ public class AtTests extends Common {
BigDecimal messageFee = messageTransaction.getTransactionData().getFee();
BigDecimal deployersPostDeploymentBalance = deployersInitialBalance.subtract(fundingAmount).subtract(deployAtFee).subtract(messageFee);
checkAtRefund(repository, deployer, deployersInitialBalance, deployAtFee);
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
describeAt(repository, atAddress);
@@ -340,7 +354,7 @@ public class AtTests extends Common {
describeAt(repository, atAddress);
checkAtRefund(repository, deployer, deployersInitialBalance, deployAtFee);
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
}
}
@@ -382,7 +396,7 @@ public class AtTests extends Common {
describeAt(repository, atAddress);
checkAtRefund(repository, deployer, deployersInitialBalance, deployAtFee);
checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
}
}
@@ -421,7 +435,7 @@ public class AtTests extends Common {
}
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer) throws DataException {
byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), secretHash, refundTimeout, refundTimeout, initialPayout, redeemAmount, bitcoinAmount);
byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), secretHash, refundTimeout, initialPayout, redeemAmount, bitcoinAmount);
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = deployer.getLastReference();
@@ -475,7 +489,7 @@ public class AtTests extends Common {
return messageTransaction;
}
private void checkAtRefund(Repository repository, Account deployer, BigDecimal deployersInitialBalance, BigDecimal deployAtFee) throws DataException {
private void checkTradeRefund(Repository repository, Account deployer, BigDecimal deployersInitialBalance, BigDecimal deployAtFee) throws DataException {
BigDecimal deployersPostDeploymentBalance = deployersInitialBalance.subtract(fundingAmount).subtract(deployAtFee);
// AT should automatically refund deployer after 'refundTimeout' blocks
@@ -520,7 +534,6 @@ public class AtTests extends Common {
+ "\tinitial payout: %s QORT,\n"
+ "\tredeem payout: %s QORT,\n"
+ "\texpected bitcoin: %s BTC,\n"
+ "\toffer timeout: %d minutes (from creation),\n"
+ "\ttrade timeout: %d minutes (from trade start),\n"
+ "\tcurrent block height: %d,\n",
tradeData.qortalAddress,
@@ -531,18 +544,17 @@ public class AtTests extends Common {
tradeData.initialPayout.toPlainString(),
tradeData.redeemPayout.toPlainString(),
tradeData.expectedBitcoin.toPlainString(),
tradeData.offerRefundTimeout,
tradeData.tradeRefundTimeout,
currentBlockHeight));
// Are we in 'offer' or 'trade' stage?
if (tradeData.tradeRefundHeight == null) {
// Offer
System.out.println(String.format("\toffer timeout: block %d",
tradeData.offerRefundHeight));
System.out.println(String.format("\tstatus: 'offer mode'"));
} else {
// Trade
System.out.println(String.format("\ttrade timeout: block %d,\n"
System.out.println(String.format("\tstatus: 'trade mode',\n"
+ "\ttrade timeout: block %d,\n"
+ "\ttrade recipient: %s",
tradeData.tradeRefundHeight,
tradeData.qortalRecipient));

View File

@@ -54,7 +54,7 @@ public class BuildP2SH {
Address redeemBitcoinAddress = null;
byte[] secretHash = null;
int lockTime = 0;
Coin bitcoinFee = BTCACCT.DEFAULT_BTC_FEE;
Coin bitcoinFee = Common.DEFAULT_BTC_FEE;
int argIndex = 0;
try {
@@ -74,8 +74,8 @@ public class BuildP2SH {
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 (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++]);

View File

@@ -58,7 +58,7 @@ public class CheckP2SH {
Address redeemBitcoinAddress = null;
byte[] secretHash = null;
int lockTime = 0;
Coin bitcoinFee = BTCACCT.DEFAULT_BTC_FEE;
Coin bitcoinFee = Common.DEFAULT_BTC_FEE;
int argIndex = 0;
try {

View File

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

View File

@@ -34,19 +34,20 @@ public class DeployAT {
if (error != null)
System.err.println(error);
System.err.println(String.format("usage: DeployAT <your Qortal PRIVATE key> <QORT amount> <BTC amount> <HASH160-of-secret> [<initial QORT payout> [<AT funding amount>]]"));
System.err.println(String.format("usage: DeployAT <your Qortal PRIVATE key> <QORT amount> <BTC amount> <HASH160-of-secret> <initial QORT payout> <AT funding amount> <AT trade timeout>"));
System.err.println(String.format("example: DeployAT "
+ "AdTd9SUEYSdTW8mgK3Gu72K97bCHGdUwi2VvLNjUohot \\\n"
+ "\t80.4020 \\\n"
+ "\t0.00864200 \\\n"
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
+ "\t0.0001 \\\n"
+ "\t123.456"));
+ "\t123.456 \\\n"
+ "\t10"));
System.exit(1);
}
public static void main(String[] args) {
if (args.length < 5 || args.length > 7)
if (args.length != 8)
usage(null);
Security.insertProviderAt(new BouncyCastleProvider(), 0);
@@ -58,6 +59,7 @@ public class DeployAT {
byte[] secretHash = null;
BigDecimal initialPayout = BigDecimal.ZERO.setScale(8);
BigDecimal fundingAmount = null;
int tradeTimeout = 0;
int argIndex = 0;
try {
@@ -77,15 +79,15 @@ public class DeployAT {
if (secretHash.length != 20)
usage("Hash of secret must be 20 bytes");
if (args.length > argIndex)
initialPayout = new BigDecimal(args[argIndex++]).setScale(8);
initialPayout = new BigDecimal(args[argIndex++]).setScale(8);
if (args.length > argIndex) {
fundingAmount = new BigDecimal(args[argIndex++]).setScale(8);
fundingAmount = new BigDecimal(args[argIndex++]).setScale(8);
if (fundingAmount.compareTo(redeemAmount) <= 0)
usage("AT funding amount must be greater than QORT redeem amount");
if (fundingAmount.compareTo(redeemAmount) <= 0)
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()));
}
@@ -105,17 +107,12 @@ public class DeployAT {
System.out.println(String.format("QORT redeem amount: %s", redeemAmount.toPlainString()));
if (fundingAmount == null)
fundingAmount = redeemAmount.add(atFundingExtra);
System.out.println(String.format("AT funding amount: %s", fundingAmount.toPlainString()));
System.out.println(String.format("HASH160 of secret: %s", HashCode.fromBytes(secretHash)));
// Deploy AT
final int offerTimeout = 2 * 60; // minutes
final int tradeTimeout = 60; // minutes
byte[] creationBytes = BTCACCT.buildQortalAT(refundAccount.getAddress(), secretHash, offerTimeout, tradeTimeout, initialPayout, fundingAmount, expectedBitcoin);
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();
@@ -133,7 +130,7 @@ public class DeployAT {
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, redeemAmount, Asset.QORT);
TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT);
Transaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);

View File

@@ -64,7 +64,7 @@ public class Redeem {
byte[] redeemPrivateKey = null;
byte[] secret = null;
int lockTime = 0;
Coin bitcoinFee = BTCACCT.DEFAULT_BTC_FEE;
Coin bitcoinFee = Common.DEFAULT_BTC_FEE;
int argIndex = 0;
try {

View File

@@ -64,7 +64,7 @@ public class Refund {
Address redeemBitcoinAddress = null;
byte[] secretHash = null;
int lockTime = 0;
Coin bitcoinFee = BTCACCT.DEFAULT_BTC_FEE;
Coin bitcoinFee = Common.DEFAULT_BTC_FEE;
int argIndex = 0;
try {