mirror of
https://github.com/Qortal/qortal.git
synced 2025-07-22 20:26:50 +00:00
WIP: split P2SH from BTCACCT, add more fields to TradeBotData, remove initial QORT payout
This commit is contained in:
@@ -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)
|
||||
|
@@ -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)
|
||||
|
@@ -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);
|
||||
|
@@ -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
Normal file
232
src/main/java/org/qortal/crosschain/BTCP2SH.java
Normal file
@@ -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;
|
||||
}
|
||||
|
||||
}
|
@@ -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;
|
||||
|
@@ -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() {
|
||||
|
@@ -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 {
|
||||
|
@@ -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:
|
||||
|
Reference in New Issue
Block a user