Browse Source

WIP: split P2SH from BTCACCT, add more fields to TradeBotData, remove initial QORT payout

split-DB
catbref 4 years ago
parent
commit
da254058c5
  1. 6
      src/main/java/org/qortal/api/model/CrossChainBuildRequest.java
  2. 24
      src/main/java/org/qortal/api/resource/CrossChainResource.java
  3. 27
      src/main/java/org/qortal/controller/TradeBot.java
  4. 253
      src/main/java/org/qortal/crosschain/BTCACCT.java
  5. 232
      src/main/java/org/qortal/crosschain/BTCP2SH.java
  6. 8
      src/main/java/org/qortal/data/crosschain/CrossChainTradeData.java
  7. 46
      src/main/java/org/qortal/data/crosschain/TradeBotData.java
  8. 33
      src/main/java/org/qortal/repository/hsqldb/HSQLDBCrossChainRepository.java
  9. 4
      src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java
  10. 59
      src/test/java/org/qortal/test/btcacct/AtTests.java
  11. 4
      src/test/java/org/qortal/test/btcacct/BtcTests.java
  12. 4
      src/test/java/org/qortal/test/btcacct/BuildP2SH.java
  13. 4
      src/test/java/org/qortal/test/btcacct/CheckP2SH.java
  14. 12
      src/test/java/org/qortal/test/btcacct/DeployAT.java
  15. 6
      src/test/java/org/qortal/test/btcacct/Redeem.java
  16. 6
      src/test/java/org/qortal/test/btcacct/Refund.java

6
src/main/java/org/qortal/api/model/CrossChainBuildRequest.java

@ -12,13 +12,9 @@ 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;
public long qortAmount;
@Schema(description = "QORT amount funding AT, including covering AT execution fees", example = "123.45670000")
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)

24
src/main/java/org/qortal/api/resource/CrossChainResource.java

@ -48,6 +48,7 @@ import org.qortal.asset.Asset;
import org.qortal.controller.TradeBot;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCACCT;
import org.qortal.crosschain.BTCP2SH;
import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData;
import org.qortal.data.crosschain.CrossChainTradeData;
@ -162,17 +163,14 @@ public class CrossChainResource {
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)
if (tradeRequest.qortAmount <= 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)
if (tradeRequest.fundingQortAmount <= tradeRequest.qortAmount)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
if (tradeRequest.bitcoinAmount <= 0)
@ -182,7 +180,7 @@ public class CrossChainResource {
PublicKeyAccount creatorAccount = new PublicKeyAccount(repository, creatorPublicKey);
byte[] creationBytes = BTCACCT.buildQortalAT(creatorAccount.getAddress(), tradeRequest.bitcoinPublicKeyHash, tradeRequest.secretHash,
tradeRequest.tradeTimeout, tradeRequest.initialQortAmount, tradeRequest.finalQortAmount, tradeRequest.bitcoinAmount);
tradeRequest.tradeTimeout, tradeRequest.qortAmount, tradeRequest.bitcoinAmount);
long txTimestamp = NTP.getTime();
byte[] lastReference = creatorAccount.getLastReference();
@ -434,7 +432,7 @@ public class CrossChainResource {
if (crossChainTradeData.mode == Mode.OFFER)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
byte[] redeemScriptBytes = BTCACCT.buildScript(templateRequest.refundPublicKeyHash, crossChainTradeData.lockTime, templateRequest.redeemPublicKeyHash, crossChainTradeData.secretHash);
byte[] redeemScriptBytes = BTCP2SH.buildScript(templateRequest.refundPublicKeyHash, crossChainTradeData.lockTime, templateRequest.redeemPublicKeyHash, crossChainTradeData.secretHash);
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
@ -485,7 +483,7 @@ public class CrossChainResource {
if (crossChainTradeData.mode == Mode.OFFER)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
byte[] redeemScriptBytes = BTCACCT.buildScript(templateRequest.refundPublicKeyHash, crossChainTradeData.lockTime, templateRequest.redeemPublicKeyHash, crossChainTradeData.secretHash);
byte[] redeemScriptBytes = BTCP2SH.buildScript(templateRequest.refundPublicKeyHash, crossChainTradeData.lockTime, templateRequest.redeemPublicKeyHash, crossChainTradeData.secretHash);
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
@ -516,7 +514,7 @@ public class CrossChainResource {
if (now >= medianBlockTime * 1000L) {
// See if we can extract secret
List<byte[]> rawTransactions = BTC.getInstance().getAddressTransactions(p2shStatus.bitcoinP2shAddress);
p2shStatus.secret = BTCACCT.findP2shSecret(p2shStatus.bitcoinP2shAddress, rawTransactions);
p2shStatus.secret = BTCP2SH.findP2shSecret(p2shStatus.bitcoinP2shAddress, rawTransactions);
}
return p2shStatus;
@ -582,7 +580,7 @@ public class CrossChainResource {
if (crossChainTradeData.mode == Mode.OFFER)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
byte[] redeemScriptBytes = BTCACCT.buildScript(refundKey.getPubKeyHash(), crossChainTradeData.lockTime, refundRequest.redeemPublicKeyHash, crossChainTradeData.secretHash);
byte[] redeemScriptBytes = BTCP2SH.buildScript(refundKey.getPubKeyHash(), crossChainTradeData.lockTime, refundRequest.redeemPublicKeyHash, crossChainTradeData.secretHash);
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
@ -608,7 +606,7 @@ public class CrossChainResource {
Coin refundAmount = Coin.valueOf(p2shBalance - refundRequest.bitcoinMinerFee.unscaledValue().longValue());
org.bitcoinj.core.Transaction refundTransaction = BTCACCT.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, crossChainTradeData.lockTime);
org.bitcoinj.core.Transaction refundTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, crossChainTradeData.lockTime);
boolean wasBroadcast = BTC.getInstance().broadcastTransaction(refundTransaction);
if (!wasBroadcast)
@ -680,7 +678,7 @@ public class CrossChainResource {
if (crossChainTradeData.mode == Mode.OFFER)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
byte[] redeemScriptBytes = BTCACCT.buildScript(redeemRequest.refundPublicKeyHash, crossChainTradeData.lockTime, redeemKey.getPubKeyHash(), crossChainTradeData.secretHash);
byte[] redeemScriptBytes = BTCP2SH.buildScript(redeemRequest.refundPublicKeyHash, crossChainTradeData.lockTime, redeemKey.getPubKeyHash(), crossChainTradeData.secretHash);
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
@ -709,7 +707,7 @@ public class CrossChainResource {
Coin redeemAmount = Coin.valueOf(p2shBalance - redeemRequest.bitcoinMinerFee.unscaledValue().longValue());
org.bitcoinj.core.Transaction redeemTransaction = BTCACCT.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, redeemRequest.secret);
org.bitcoinj.core.Transaction redeemTransaction = BTCP2SH.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, redeemRequest.secret);
boolean wasBroadcast = BTC.getInstance().broadcastTransaction(redeemTransaction);
if (!wasBroadcast)

27
src/main/java/org/qortal/controller/TradeBot.java

@ -8,17 +8,16 @@ import java.util.Random;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
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.qortal.account.PrivateKeyAccount;
import org.qortal.account.PublicKeyAccount;
import org.qortal.api.model.TradeBotCreateRequest;
import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
import org.qortal.asset.Asset;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCACCT;
import org.qortal.crosschain.BTCP2SH;
import org.qortal.crypto.Crypto;
import org.qortal.data.at.ATData;
import org.qortal.data.crosschain.CrossChainTradeData;
@ -32,8 +31,8 @@ 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.TransactionType;
import org.qortal.transaction.Transaction.ValidationResult;
import org.qortal.transform.TransformationException;
import org.qortal.transform.transaction.DeployAtTransactionTransformer;
import org.qortal.utils.NTP;
@ -55,7 +54,7 @@ public class TradeBot {
return instance;
}
public static byte[] createTrade(Repository repository, TradeBotCreateRequest tradeBotCreateRequest) {
public static byte[] createTrade(Repository repository, TradeBotCreateRequest tradeBotCreateRequest) throws DataException {
BTC btc = BTC.getInstance();
NetworkParameters params = btc.getNetworkParameters();
@ -90,12 +89,18 @@ public class TradeBot {
String atAddress = deployAtTransactionData.getAtAddress();
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, TradeBotData.State.BOB_WAITING_FOR_MESSAGE,
atAddress, tradeBotCreateRequest.tradeTimeout,
tradeNativePublicKey, tradeNativePublicKeyHash, secret, secretHash,
tradeForeignPublicKey, tradeForeignPublicKeyHash, atAddress, null);
tradeForeignPublicKey, tradeForeignPublicKeyHash,
tradeBotCreateRequest.bitcoinAmount, null);
repository.getCrossChainRepository().save(tradeBotData);
// Return to user for signing and broadcast as we don't have their Qortal private key
return DeployAtTransactionTransformer.toBytes(deployAtTransactionData);
try {
return DeployAtTransactionTransformer.toBytes(deployAtTransactionData);
} catch (TransformationException e) {
throw new DataException("Failed to transform DEPLOY_AT transaction?", e);
}
}
public static String startResponse(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException {
@ -113,12 +118,14 @@ public class TradeBot {
byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, TradeBotData.State.ALICE_WAITING_FOR_P2SH_A,
crossChainTradeData.qortalAtAddress, crossChainTradeData.tradeTimeout,
tradeNativePublicKey, tradeNativePublicKeyHash, secret, secretHash,
tradeForeignPublicKey, tradeForeignPublicKeyHash, crossChainTradeData.qortalAtAddress, null);
tradeForeignPublicKey, tradeForeignPublicKeyHash,
crossChainTradeData.expectedBitcoin, null);
repository.getCrossChainRepository().save(tradeBotData);
// P2SH_a to be funded
byte[] redeemScriptBytes = BTCACCT.buildScript(tradeForeignPublicKeyHash, crossChainTradeData.lockTime, crossChainTradeData.foreignPublicKeyHash, secretHash);
byte[] redeemScriptBytes = BTCP2SH.buildScript(tradeForeignPublicKeyHash, crossChainTradeData.lockTime, crossChainTradeData.foreignPublicKeyHash, secretHash);
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
@ -176,7 +183,7 @@ public class TradeBot {
repository.getCrossChainRepository().save(tradeBotData);
}
private void handleBobWaitingForMessage(Repository repository, TradeBotData tradeBotData) {
private void handleBobWaitingForMessage(Repository repository, TradeBotData tradeBotData) throws DataException {
// Fetch AT so we can determine trade start timestamp
ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress());
if (atData == null) {
@ -220,7 +227,7 @@ public class TradeBot {
// Determine P2SH address and confirm funded
int lockTime = (int) (tradeStartTimestamp / 1000L + tradeBotData.getTradeTimeout() / 4 * 60); // First P2SH locktime is ¼ of timeout period
byte[] redeemScript = BTCACCT.buildScript(aliceForeignPublicKeyHash, lockTime, tradeBotData.getTradeForeignPublicKeyHash(), aliceSecretHash);
byte[] redeemScript = BTCP2SH.buildScript(aliceForeignPublicKeyHash, lockTime, tradeBotData.getTradeForeignPublicKeyHash(), aliceSecretHash);
String p2shAddress = BTC.getInstance().deriveP2shAddress(redeemScript);
Long balance = BTC.getInstance().getBalance(p2shAddress);

253
src/main/java/org/qortal/crosschain/BTCACCT.java

@ -4,23 +4,7 @@ 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.LegacyAddress;
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.ciyam.at.API;
import org.ciyam.at.CompilationException;
import org.ciyam.at.FunctionCode;
@ -40,7 +24,6 @@ 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;
@ -72,164 +55,6 @@ public class BTCACCT {
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)
* <push 20 bytes> <refund PKH> OP_EQUAL (does PKH match refund PKH?)
* OP_IF
* OP_DROP (no need for duplicate PKH)
* <push 4 bytes> <locktime>
* OP_CHECKLOCKTIMEVERIFY (if this passes, leftover stack is <locktime> so script passes)
* OP_ELSE
* <push 20 bytes> <redeem PKH> OP_EQUALVERIFY (duplicate PKH must match redeem PKH or script fails)
* OP_HASH160 (hash secret)
* <push 20 bytes> <hash of 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.
* <p>
* 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, List<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes, Long lockTime, Function<byte[], Script> 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()));
for (int inputIndex = 0; inputIndex < fundingOutputs.size(); ++inputIndex) {
TransactionOutput fundingOutput = fundingOutputs.get(inputIndex);
// 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);
for (int inputIndex = 0; inputIndex < fundingOutputs.size(); ++inputIndex) {
// Generate transaction signature for input
final boolean anyoneCanPay = false;
TransactionSignature txSig = transaction.calculateSignature(inputIndex, 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(inputIndex).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, List<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes, long lockTime) {
Function<byte[], Script> 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, fundingOutputs, 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, List<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes, byte[] secret) {
Function<byte[], Script> 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, fundingOutputs, redeemScriptBytes, null, redeemSigScriptBuilder);
}
/**
* Returns Qortal AT creation bytes for cross-chain trading AT.
* <p>
@ -240,12 +65,11 @@ public class BTCACCT {
* @param bitcoinPublicKeyHash 20-byte HASH160 of creator's bitcoin public key
* @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 qortAmount 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[] bitcoinPublicKeyHash, byte[] secretHash, int tradeTimeout, long initialPayout, long redeemPayout, long bitcoinAmount) {
public static byte[] buildQortalAT(String qortalCreator, byte[] bitcoinPublicKeyHash, byte[] secretHash, int tradeTimeout, long qortAmount, long bitcoinAmount) {
// Labels for data segment addresses
int addrCounter = 0;
@ -263,8 +87,7 @@ public class BTCACCT {
addrCounter += 4;
final int addrTradeTimeout = addrCounter++;
final int addrInitialPayoutAmount = addrCounter++;
final int addrRedeemPayoutAmount = addrCounter++;
final int addrQortAmount = addrCounter++;
final int addrBitcoinAmount = addrCounter++;
final int addrMessageTxType = addrCounter++;
@ -319,13 +142,9 @@ public class BTCACCT {
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);
// Redeem Qort amount
assert dataByteBuffer.position() == addrQortAmount * MachineState.VALUE_SIZE : "addrQortAmount incorrect";
dataByteBuffer.putLong(qortAmount);
// Expected Bitcoin amount
assert dataByteBuffer.position() == addrBitcoinAmount * MachineState.VALUE_SIZE : "addrBitcoinAmount incorrect";
@ -433,9 +252,6 @@ public class BTCACCT {
/* 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));
@ -504,7 +320,7 @@ public class BTCACCT {
// 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));
codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PAY_TO_ADDRESS_IN_B, addrQortAmount));
// Fall-through to refunding any remaining balance back to AT creator
/* Refund balance back to AT creator */
@ -578,13 +394,10 @@ public class BTCACCT {
dataByteBuffer.position(dataByteBuffer.position() + 32 - 20); // skip to 32 bytes
// Trade timeout
tradeData.tradeRefundTimeout = dataByteBuffer.getLong();
// Initial payout
tradeData.initialPayout = dataByteBuffer.getLong();
tradeData.tradeTimeout = (int) dataByteBuffer.getLong();
// Redeem payout
tradeData.redeemPayout = dataByteBuffer.getLong();
tradeData.qortAmount = dataByteBuffer.getLong();
// Expected BTC amount
tradeData.expectedBitcoin = dataByteBuffer.getLong();
@ -623,12 +436,12 @@ public class BTCACCT {
// We'll suggest half of trade timeout
CiyamAtSettings ciyamAtSettings = BlockChain.getInstance().getCiyamAtSettings();
int tradeModeSwitchHeight = (int) (tradeData.tradeRefundHeight - tradeData.tradeRefundTimeout / ciyamAtSettings.minutesPerBlock);
int tradeModeSwitchHeight = (int) (tradeData.tradeRefundHeight - tradeData.tradeTimeout / 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);
tradeData.lockTime = (int) (tradeData.tradeModeTimestamp / 1000L + tradeData.tradeTimeout / 2 * 60);
}
} else {
tradeData.mode = CrossChainTradeData.Mode.OFFER;
@ -637,46 +450,4 @@ public class BTCACCT {
return tradeData;
}
public static byte[] findP2shSecret(String p2shAddress, List<byte[]> rawTransactions) {
NetworkParameters params = BTC.getInstance().getNetworkParameters();
for (byte[] rawTransaction : rawTransactions) {
Transaction transaction = new Transaction(params, rawTransaction);
// Cycle through inputs, looking for one that spends our P2SH
for (TransactionInput input : transaction.getInputs()) {
Script scriptSig = input.getScriptSig();
List<ScriptChunk> scriptChunks = scriptSig.getChunks();
// Expected number of script chunks for redeem. Refund might not have the same number.
int expectedChunkCount = 1 /*secret*/ + 1 /*sig*/ + 1 /*pubkey*/ + 1 /*redeemScript*/;
if (scriptChunks.size() != expectedChunkCount)
continue;
// We're expecting last chunk to contain the actual redeemScript
ScriptChunk lastChunk = scriptChunks.get(scriptChunks.size() - 1);
byte[] redeemScriptBytes = lastChunk.data;
// If non-push scripts, redeemScript will be null
if (redeemScriptBytes == null)
continue;
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
Address inputAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
if (!inputAddress.toString().equals(p2shAddress))
// Input isn't spending our P2SH
continue;
byte[] secret = scriptChunks.get(0).data;
if (secret.length != BTCACCT.SECRET_LENGTH)
continue;
return secret;
}
}
return null;
}
}

232
src/main/java/org/qortal/crosschain/BTCP2SH.java

@ -0,0 +1,232 @@
package org.qortal.crosschain;
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.LegacyAddress;
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.qortal.crypto.Crypto;
import org.qortal.utils.BitTwiddling;
import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes;
public class BTCP2SH {
public static final int SECRET_LENGTH = 32;
public static final int MIN_LOCKTIME = 1500000000;
/*
* 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)
* <push 20 bytes> <refund PKH> OP_EQUAL (does PKH match refund PKH?)
* OP_IF
* OP_DROP (no need for duplicate PKH)
* <push 4 bytes> <locktime>
* OP_CHECKLOCKTIMEVERIFY (if this passes, leftover stack is <locktime> so script passes)
* OP_ELSE
* <push 20 bytes> <redeem PKH> OP_EQUALVERIFY (duplicate PKH must match redeem PKH or script fails)
* OP_HASH160 (hash secret)
* <push 20 bytes> <hash of 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.
* <p>
* See comments in {@link BTCP2SH} 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, List<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes, Long lockTime, Function<byte[], Script> 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()));
for (int inputIndex = 0; inputIndex < fundingOutputs.size(); ++inputIndex) {
TransactionOutput fundingOutput = fundingOutputs.get(inputIndex);
// 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);
for (int inputIndex = 0; inputIndex < fundingOutputs.size(); ++inputIndex) {
// Generate transaction signature for input
final boolean anyoneCanPay = false;
TransactionSignature txSig = transaction.calculateSignature(inputIndex, 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(inputIndex).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, List<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes, long lockTime) {
Function<byte[], Script> 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, fundingOutputs, 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, List<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes, byte[] secret) {
Function<byte[], Script> 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, fundingOutputs, redeemScriptBytes, null, redeemSigScriptBuilder);
}
/** Returns 'secret', if any, given list of raw bitcoin transactions. */
public static byte[] findP2shSecret(String p2shAddress, List<byte[]> rawTransactions) {
NetworkParameters params = BTC.getInstance().getNetworkParameters();
for (byte[] rawTransaction : rawTransactions) {
Transaction transaction = new Transaction(params, rawTransaction);
// Cycle through inputs, looking for one that spends our P2SH
for (TransactionInput input : transaction.getInputs()) {
Script scriptSig = input.getScriptSig();
List<ScriptChunk> scriptChunks = scriptSig.getChunks();
// Expected number of script chunks for redeem. Refund might not have the same number.
int expectedChunkCount = 1 /*secret*/ + 1 /*sig*/ + 1 /*pubkey*/ + 1 /*redeemScript*/;
if (scriptChunks.size() != expectedChunkCount)
continue;
// We're expecting last chunk to contain the actual redeemScript
ScriptChunk lastChunk = scriptChunks.get(scriptChunks.size() - 1);
byte[] redeemScriptBytes = lastChunk.data;
// If non-push scripts, redeemScript will be null
if (redeemScriptBytes == null)
continue;
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
Address inputAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
if (!inputAddress.toString().equals(p2shAddress))
// Input isn't spending our P2SH
continue;
byte[] secret = scriptChunks.get(0).data;
if (secret.length != BTCP2SH.SECRET_LENGTH)
continue;
return secret;
}
}
return null;
}
}

8
src/main/java/org/qortal/data/crosschain/CrossChainTradeData.java

@ -30,13 +30,9 @@ public class CrossChainTradeData {
@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;
public long qortAmount;
@Schema(description = "Trade partner's Qortal address (trade begins when this is set)")
public String qortalRecipient;
@ -45,7 +41,7 @@ public class CrossChainTradeData {
public Long tradeModeTimestamp;
@Schema(description = "How long from beginning trade until AT triggers automatic refund to AT creator (minutes)")
public long tradeRefundTimeout;
public int tradeTimeout;
@Schema(description = "Actual Qortal block height when AT will automatically refund to AT creator (after trade begins)")
public Integer tradeRefundHeight;

46
src/main/java/org/qortal/data/crosschain/TradeBotData.java

@ -15,6 +15,11 @@ import io.swagger.v3.oas.annotations.media.Schema;
@XmlAccessorType(XmlAccessType.FIELD)
public class TradeBotData {
// Never expose this
@XmlTransient
@Schema(hidden = true)
private byte[] tradePrivateKey;
public enum State {
BOB_WAITING_FOR_AT_CONFIRM(10), BOB_WAITING_FOR_MESSAGE(20), BOB_SENDING_MESSAGE_TO_AT(30), BOB_WAITING_FOR_P2SH_B(40), BOB_WAITING_FOR_AT_REDEEM(50),
ALICE_WAITING_FOR_P2SH_A(110), ALICE_WAITING_FOR_AT_LOCK(120), ALICE_WATCH_P2SH_B(130);
@ -30,13 +35,10 @@ public class TradeBotData {
return map.get(value);
}
}
private State tradeState;
// Never expose this
@XmlTransient
@Schema(hidden = true)
private byte[] tradePrivateKey;
private String atAddress;
private int tradeTimeout;
private byte[] tradeNativePublicKey;
private byte[] tradeNativePublicKeyHash;
@ -47,23 +49,25 @@ public class TradeBotData {
private byte[] tradeForeignPublicKey;
private byte[] tradeForeignPublicKeyHash;
private String atAddress;
private long bitcoinAmount;
private byte[] lastTransactionSignature;
public TradeBotData(byte[] tradePrivateKey, State tradeState,
byte[] tradeNativePublicKey, byte[] tradeNativePublicKeyHash, byte[] secret, byte[] secretHash,
byte[] tradeForeignPublicKey, byte[] tradeForeignPublicKeyHash, String atAddress,
byte[] lastTransactionSignature) {
public TradeBotData(byte[] tradePrivateKey, State tradeState, String atAddress, int tradeTimeout,
byte[] tradeNativePublicKey, byte[] tradeNativePublicKeyHash, byte[] secret, byte[] secretHash,
byte[] tradeForeignPublicKey, byte[] tradeForeignPublicKeyHash,
long bitcoinAmount, byte[] lastTransactionSignature) {
this.tradePrivateKey = tradePrivateKey;
this.tradeState = tradeState;
this.atAddress = atAddress;
this.tradeTimeout = tradeTimeout;
this.tradeNativePublicKey = tradeNativePublicKey;
this.tradeNativePublicKeyHash = tradeNativePublicKeyHash;
this.secret = secret;
this.secretHash = secretHash;
this.tradeForeignPublicKey = tradeForeignPublicKey;
this.tradeForeignPublicKeyHash = tradeForeignPublicKeyHash;
this.atAddress = atAddress;
this.bitcoinAmount = bitcoinAmount;
this.lastTransactionSignature = lastTransactionSignature;
}
@ -79,6 +83,18 @@ public class TradeBotData {
this.tradeState = state;
}
public String getAtAddress() {
return this.atAddress;
}
public void setAtAddress(String atAddress) {
this.atAddress = atAddress;
}
public int getTradeTimeout() {
return this.tradeTimeout;
}
public byte[] getTradeNativePublicKey() {
return this.tradeNativePublicKey;
}
@ -103,12 +119,8 @@ public class TradeBotData {
return this.tradeForeignPublicKeyHash;
}
public String getAtAddress() {
return this.atAddress;
}
public void setAtAddress(String atAddress) {
this.atAddress = atAddress;
public long getBitcoinAmount() {
return this.bitcoinAmount;
}
public byte[] getLastTransactionSignature() {

33
src/main/java/org/qortal/repository/hsqldb/HSQLDBCrossChainRepository.java

@ -19,8 +19,11 @@ public class HSQLDBCrossChainRepository implements CrossChainRepository {
@Override
public List<TradeBotData> getAllTradeBotData() throws DataException {
String sql = "SELECT trade_private_key, trade_state, trade_native_public_key, trade_native_public_key_hash, "
+ "secret, secret_hash, trade_foreign_public_key, trade_foreign_public_key_hash, at_address, last_transaction_signature "
String sql = "SELECT trade_private_key, trade_state, at_address, trade_timeout, "
+ "trade_native_public_key, trade_native_public_key_hash, "
+ "secret, secret_hash, "
+ "trade_foreign_public_key, trade_foreign_public_key_hash, "
+ "bitcoin_amount, last_transaction_signature "
+ "FROM TradeBotStates";
List<TradeBotData> allTradeBotData = new ArrayList<>();
@ -36,18 +39,22 @@ public class HSQLDBCrossChainRepository implements CrossChainRepository {
if (tradeState == null)
throw new DataException("Illegal trade-bot trade-state fetched from repository");
byte[] tradeNativePublicKey = resultSet.getBytes(3);
byte[] tradeNativePublicKeyHash = resultSet.getBytes(4);
byte[] secret = resultSet.getBytes(5);
byte[] secretHash = resultSet.getBytes(6);
byte[] tradeForeignPublicKey = resultSet.getBytes(7);
byte[] tradeForeignPublicKeyHash = resultSet.getBytes(8);
String atAddress = resultSet.getString(9);
byte[] lastTransactionSignature = resultSet.getBytes(10);
String atAddress = resultSet.getString(3);
int tradeTimeout = resultSet.getInt(4);
byte[] tradeNativePublicKey = resultSet.getBytes(5);
byte[] tradeNativePublicKeyHash = resultSet.getBytes(6);
byte[] secret = resultSet.getBytes(7);
byte[] secretHash = resultSet.getBytes(8);
byte[] tradeForeignPublicKey = resultSet.getBytes(9);
byte[] tradeForeignPublicKeyHash = resultSet.getBytes(10);
long bitcoinAmount = resultSet.getLong(11);
byte[] lastTransactionSignature = resultSet.getBytes(12);
TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, tradeState,
atAddress, tradeTimeout,
tradeNativePublicKey, tradeNativePublicKeyHash, secret, secretHash,
tradeForeignPublicKey, tradeForeignPublicKeyHash, atAddress, lastTransactionSignature);
tradeForeignPublicKey, tradeForeignPublicKeyHash,
bitcoinAmount, lastTransactionSignature);
allTradeBotData.add(tradeBotData);
} while (resultSet.next());
@ -63,12 +70,14 @@ public class HSQLDBCrossChainRepository implements CrossChainRepository {
saveHelper.bind("trade_private_key", tradeBotData.getTradePrivateKey())
.bind("trade_state", tradeBotData.getState().value)
.bind("at_address", tradeBotData.getAtAddress())
.bind("trade_timeout", tradeBotData.getTradeTimeout())
.bind("trade_native_public_key", tradeBotData.getTradeNativePublicKey())
.bind("trade_native_public_key_hash", tradeBotData.getTradeNativePublicKeyHash())
.bind("secret", tradeBotData.getSecret()).bind("secret_hash", tradeBotData.getSecretHash())
.bind("trade_foreign_public_key", tradeBotData.getTradeForeignPublicKey())
.bind("trade_foreign_public_key_hash", tradeBotData.getTradeForeignPublicKeyHash())
.bind("at_address", tradeBotData.getAtAddress())
.bind("bitcoin_amount", tradeBotData.getBitcoinAmount())
.bind("last_transaction_signature", tradeBotData.getLastTransactionSignature());
try {

4
src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java

@ -621,11 +621,11 @@ public class HSQLDBDatabaseUpdates {
case 20:
// Trade bot
stmt.execute("CREATE TABLE TradeBotStates (trade_private_key QortalKeySeed NOT NULL, trade_state TINYINT NOT NULL, "
+ "at_address QortalAddress, trade_timeout INT NOT NULL, "
+ "trade_native_public_key QortalPublicKey NOT NULL, trade_native_public_key_hash VARBINARY(32) NOT NULL, "
+ "secret VARBINARY(32) NOT NULL, secret_hash VARBINARY(32) NOT NULL, "
+ "trade_foreign_public_key QortalPublicKey NOT NULL, trade_foreign_public_key_hash VARBINARY(32) NOT NULL, "
+ "at_address QortalAddress, "
+ "last_transaction_signature Signature, PRIMARY KEY (trade_private_key))");
+ "bitcoin_amount BIGINT NOT NULL, last_transaction_signature Signature, PRIMARY KEY (trade_private_key))");
break;
default:

59
src/test/java/org/qortal/test/btcacct/AtTests.java

@ -47,7 +47,6 @@ public class AtTests extends Common {
public static final byte[] bitcoinPublicKeyHash = new byte[20]; // not used in tests
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;
@ -61,7 +60,7 @@ public class AtTests extends Common {
public void testCompile() {
Account deployer = Common.getTestAccount(null, "chloe");
byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), bitcoinPublicKeyHash, secretHash, refundTimeout, initialPayout, redeemAmount, bitcoinAmount);
byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), bitcoinPublicKeyHash, secretHash, refundTimeout, redeemAmount, bitcoinAmount);
System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
}
@ -156,44 +155,6 @@ public class AtTests extends Common {
}
}
@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
@ -294,7 +255,7 @@ public class AtTests extends Common {
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
long expectedBalance = recipientsInitialBalance + initialPayout - messageTransaction.getTransactionData().getFee() + redeemAmount;
long expectedBalance = recipientsInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
assertEquals("Recipent's post-redeem balance incorrect", expectedBalance, actualBalance);
@ -304,7 +265,7 @@ public class AtTests extends Common {
// Orphan redeem
BlockUtils.orphanLastBlock(repository);
expectedBalance = recipientsInitialBalance + initialPayout - messageTransaction.getTransactionData().getFee();
expectedBalance = recipientsInitialBalance - messageTransaction.getTransactionData().getFee();
actualBalance = recipient.getConfirmedBalance(Asset.QORT);
assertEquals("Recipent's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance);
@ -347,7 +308,7 @@ public class AtTests extends Common {
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
long expectedBalance = recipientsInitialBalance + initialPayout;
long expectedBalance = recipientsInitialBalance;
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
assertEquals("Recipent's balance incorrect", expectedBalance, actualBalance);
@ -389,7 +350,7 @@ public class AtTests extends Common {
ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
BlockUtils.mintBlock(repository);
long expectedBalance = recipientsInitialBalance + initialPayout - messageTransaction.getTransactionData().getFee();
long expectedBalance = recipientsInitialBalance - messageTransaction.getTransactionData().getFee();
long actualBalance = recipient.getConfirmedBalance(Asset.QORT);
assertEquals("Recipent's balance incorrect", expectedBalance, actualBalance);
@ -435,7 +396,7 @@ public class AtTests extends Common {
}
private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer) throws DataException {
byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), bitcoinPublicKeyHash, secretHash, refundTimeout, initialPayout, redeemAmount, bitcoinAmount);
byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), bitcoinPublicKeyHash, secretHash, refundTimeout, redeemAmount, bitcoinAmount);
long txTimestamp = System.currentTimeMillis();
byte[] lastReference = deployer.getLastReference();
@ -501,7 +462,7 @@ public class AtTests extends Common {
// 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 expectedMaximumBalance = deployersInitialBalance - deployAtFee;
long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
@ -521,7 +482,6 @@ public class AtTests extends Common {
+ "\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"
@ -531,10 +491,9 @@ public class AtTests extends Common {
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.qortAmount),
Amounts.prettyAmount(tradeData.expectedBitcoin),
tradeData.tradeRefundTimeout,
tradeData.tradeTimeout,
currentBlockHeight));
// Are we in 'offer' or 'trade' stage?

4
src/test/java/org/qortal/test/btcacct/BtcTests.java

@ -10,7 +10,7 @@ import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCACCT;
import org.qortal.crosschain.BTCP2SH;
import org.qortal.repository.DataException;
import org.qortal.test.common.Common;
@ -56,7 +56,7 @@ public class BtcTests extends Common {
List<byte[]> rawTransactions = BTC.getInstance().getAddressTransactions(p2shAddress);
byte[] expectedSecret = AtTests.secret;
byte[] secret = BTCACCT.findP2shSecret(p2shAddress, rawTransactions);
byte[] secret = BTCP2SH.findP2shSecret(p2shAddress, rawTransactions);
assertNotNull(secret);
assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));

4
src/test/java/org/qortal/test/btcacct/BuildP2SH.java

@ -13,7 +13,7 @@ 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.crosschain.BTCP2SH;
import org.qortal.crypto.Crypto;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
@ -103,7 +103,7 @@ public class BuildP2SH {
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);
byte[] redeemScriptBytes = BTCP2SH.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);

4
src/test/java/org/qortal/test/btcacct/CheckP2SH.java

@ -15,7 +15,7 @@ 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.crosschain.BTCP2SH;
import org.qortal.crypto.Crypto;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
@ -113,7 +113,7 @@ public class CheckP2SH {
System.out.println(String.format("P2SH address: %s", p2shAddress));
byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
byte[] redeemScriptBytes = BTCP2SH.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);

12
src/test/java/org/qortal/test/btcacct/DeployAT.java

@ -34,21 +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> <your Bitcoin PKH> <HASH160-of-secret> <initial QORT payout> <AT funding amount> <AT trade timeout>"));
System.err.println(String.format("usage: DeployAT <your Qortal PRIVATE key> <QORT amount> <BTC amount> <your Bitcoin PKH> <HASH160-of-secret> <AT funding amount> <AT trade timeout>"));
System.err.println(String.format("example: DeployAT "
+ "AdTd9SUEYSdTW8mgK3Gu72K97bCHGdUwi2VvLNjUohot \\\n"
+ "\t80.4020 \\\n"
+ "\t0.00864200 \\\n"
+ "\t750b06757a2448b8a4abebaa6e4662833fd5ddbb \\\n"
+ "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n"
+ "\t0.0001 \\\n"
+ "\t123.456 \\\n"
+ "\t10"));
System.exit(1);
}
public static void main(String[] args) {
if (args.length != 8)
if (args.length != 7)
usage(null);
Security.insertProviderAt(new BouncyCastleProvider(), 0);
@ -59,7 +58,6 @@ public class DeployAT {
long expectedBitcoin = 0;
byte[] bitcoinPublicKeyHash = null;
byte[] secretHash = null;
long initialPayout = 0;
long fundingAmount = 0;
int tradeTimeout = 0;
@ -85,10 +83,6 @@ public class DeployAT {
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");
@ -120,7 +114,7 @@ public class DeployAT {
System.out.println(String.format("HASH160 of secret: %s", HashCode.fromBytes(secretHash)));
// Deploy AT
byte[] creationBytes = BTCACCT.buildQortalAT(refundAccount.getAddress(), bitcoinPublicKeyHash, secretHash, tradeTimeout, initialPayout, redeemAmount, expectedBitcoin);
byte[] creationBytes = BTCACCT.buildQortalAT(refundAccount.getAddress(), bitcoinPublicKeyHash, secretHash, tradeTimeout, redeemAmount, expectedBitcoin);
System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
long txTimestamp = System.currentTimeMillis();

6
src/test/java/org/qortal/test/btcacct/Redeem.java

@ -18,7 +18,7 @@ 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.crosschain.BTCP2SH;
import org.qortal.crypto.Crypto;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
@ -121,7 +121,7 @@ public class Redeem {
System.out.println(String.format("P2SH address: %s", p2shAddress));
byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash);
byte[] redeemScriptBytes = BTCP2SH.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash);
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
@ -182,7 +182,7 @@ public class Redeem {
Coin redeemAmount = Coin.valueOf(p2shBalance).subtract(bitcoinFee);
System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.format(redeemAmount), BTC.format(bitcoinFee)));
Transaction redeemTransaction = BTCACCT.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, secret);
Transaction redeemTransaction = BTCP2SH.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, secret);
byte[] redeemBytes = redeemTransaction.bitcoinSerialize();

6
src/test/java/org/qortal/test/btcacct/Refund.java

@ -18,7 +18,7 @@ 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.crosschain.BTCP2SH;
import org.qortal.crypto.Crypto;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
@ -120,7 +120,7 @@ public class Refund {
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);
byte[] redeemScriptBytes = BTCP2SH.buildScript(refundAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash);
System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes)));
byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
@ -186,7 +186,7 @@ public class Refund {
Coin refundAmount = Coin.valueOf(p2shBalance).subtract(bitcoinFee);
System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.format(refundAmount), BTC.format(bitcoinFee)));
Transaction redeemTransaction = BTCACCT.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime);
Transaction redeemTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime);
byte[] redeemBytes = redeemTransaction.bitcoinSerialize();

Loading…
Cancel
Save