From af7d7d096622ae97220b44eda8712cadfc2acc8d Mon Sep 17 00:00:00 2001 From: catbref Date: Wed, 16 Sep 2020 10:57:45 +0100 Subject: [PATCH] Unified Bitcoin/Litecoin test apps --- .../test/crosschain/apps/BuildHTLC.java | 112 +++++++++ .../test/crosschain/apps/CheckHTLC.java | 133 +++++++++++ .../qortal/test/crosschain/apps/Common.java | 155 +++++++++++++ .../org/qortal/test/crosschain/apps/Pay.java | 78 +++++++ .../test/crosschain/apps/RedeemHTLC.java | 164 +++++++++++++ .../test/crosschain/apps/RefundHTLC.java | 161 +++++++++++++ .../test/crosschain/bitcoinv1/BuildHTLC.java | 112 --------- .../test/crosschain/bitcoinv1/CheckHTLC.java | 161 ------------- .../test/crosschain/bitcoinv1/Common.java | 9 - .../test/crosschain/bitcoinv1/Redeem.java | 203 ---------------- .../test/crosschain/bitcoinv1/Refund.java | 207 ----------------- .../test/crosschain/litecoinv1/BuildHTLC.java | 112 --------- .../test/crosschain/litecoinv1/Refund.java | 218 ------------------ .../test/crosschain/litecoinv1/SendLTC.java | 74 ------ 14 files changed, 803 insertions(+), 1096 deletions(-) create mode 100644 src/test/java/org/qortal/test/crosschain/apps/BuildHTLC.java create mode 100644 src/test/java/org/qortal/test/crosschain/apps/CheckHTLC.java create mode 100644 src/test/java/org/qortal/test/crosschain/apps/Common.java create mode 100644 src/test/java/org/qortal/test/crosschain/apps/Pay.java create mode 100644 src/test/java/org/qortal/test/crosschain/apps/RedeemHTLC.java create mode 100644 src/test/java/org/qortal/test/crosschain/apps/RefundHTLC.java delete mode 100644 src/test/java/org/qortal/test/crosschain/bitcoinv1/BuildHTLC.java delete mode 100644 src/test/java/org/qortal/test/crosschain/bitcoinv1/CheckHTLC.java delete mode 100644 src/test/java/org/qortal/test/crosschain/bitcoinv1/Common.java delete mode 100644 src/test/java/org/qortal/test/crosschain/bitcoinv1/Redeem.java delete mode 100644 src/test/java/org/qortal/test/crosschain/bitcoinv1/Refund.java delete mode 100644 src/test/java/org/qortal/test/crosschain/litecoinv1/BuildHTLC.java delete mode 100644 src/test/java/org/qortal/test/crosschain/litecoinv1/Refund.java delete mode 100644 src/test/java/org/qortal/test/crosschain/litecoinv1/SendLTC.java diff --git a/src/test/java/org/qortal/test/crosschain/apps/BuildHTLC.java b/src/test/java/org/qortal/test/crosschain/apps/BuildHTLC.java new file mode 100644 index 00000000..5478160f --- /dev/null +++ b/src/test/java/org/qortal/test/crosschain/apps/BuildHTLC.java @@ -0,0 +1,112 @@ +package org.qortal.test.crosschain.apps; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.script.Script.ScriptType; +import org.qortal.crosschain.Litecoin; +import org.qortal.crosschain.Bitcoin; +import org.qortal.crosschain.Bitcoiny; +import org.qortal.crosschain.BitcoinyHTLC; + +import com.google.common.hash.HashCode; + +public class BuildHTLC { + + private static void usage(String error) { + if (error != null) + System.err.println(error); + + System.err.println(String.format("usage: BuildHTLC (-b | -l) ")); + System.err.println("where: -b means use Bitcoin, -l means use Litecoin"); + System.err.println(String.format("example: BuildHTLC -l " + + "msAfaDaJ8JiprxxFaAXEEPxKK3JaZCYpLv \\\n" + + "\t0.00008642 \\\n" + + "\tmrBpZYYGYMwUa8tRjTiXfP1ySqNXszWN5h \\\n" + + "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n" + + "\t1600000000")); + System.exit(1); + } + + public static void main(String[] args) { + if (args.length < 6 || args.length > 6) + usage(null); + + Common.init(); + + Bitcoiny bitcoiny = null; + NetworkParameters params = null; + + Address refundAddress = null; + Coin amount = null; + Address redeemAddress = null; + byte[] secretHash = null; + int lockTime = 0; + + int argIndex = 0; + try { + switch (args[argIndex++]) { + case "-b": + bitcoiny = Bitcoin.getInstance(); + break; + + case "-l": + bitcoiny = Litecoin.getInstance(); + break; + + default: + usage("Only Bitcoin (-b) or Litecoin (-l) supported"); + } + params = bitcoiny.getNetworkParameters(); + + refundAddress = Address.fromString(params, args[argIndex++]); + if (refundAddress.getOutputScriptType() != ScriptType.P2PKH) + usage("Refund address must be in P2PKH form"); + + amount = Coin.parseCoin(args[argIndex++]); + + redeemAddress = Address.fromString(params, args[argIndex++]); + if (redeemAddress.getOutputScriptType() != ScriptType.P2PKH) + usage("Redeem address must be in P2PKH form"); + + secretHash = HashCode.fromString(args[argIndex++]).asBytes(); + if (secretHash.length != 20) + usage("Hash of secret must be 20 bytes"); + + lockTime = Integer.parseInt(args[argIndex++]); + int refundTimeoutDelay = lockTime - (int) (System.currentTimeMillis() / 1000L); + if (refundTimeoutDelay < 600 || refundTimeoutDelay > 30 * 24 * 60 * 60) + usage("Locktime (seconds) should be at between 10 minutes and 1 month from now"); + } catch (IllegalArgumentException e) { + usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); + } + + Coin p2shFee = Coin.valueOf(Common.getP2shFee(bitcoiny)); + if (p2shFee.isZero()) + return; + + System.out.println(String.format("Refund address: %s", refundAddress)); + System.out.println(String.format("Amount: %s", amount.toPlainString())); + System.out.println(String.format("Redeem address: %s", redeemAddress)); + System.out.println(String.format("Refund/redeem miner's fee: %s", bitcoiny.format(p2shFee))); + System.out.println(String.format("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 = BitcoinyHTLC.buildScript(refundAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash); + System.out.println(String.format("Raw script bytes: %s", HashCode.fromBytes(redeemScriptBytes))); + + String p2shAddress = bitcoiny.deriveP2shAddress(redeemScriptBytes); + System.out.println(String.format("P2SH address: %s", p2shAddress)); + + amount = amount.add(p2shFee); + + // Fund P2SH + System.out.println(String.format("\nYou need to fund %s with %s (includes redeem/refund fee of %s)", + p2shAddress, bitcoiny.format(amount), bitcoiny.format(p2shFee))); + } + +} diff --git a/src/test/java/org/qortal/test/crosschain/apps/CheckHTLC.java b/src/test/java/org/qortal/test/crosschain/apps/CheckHTLC.java new file mode 100644 index 00000000..e034cec8 --- /dev/null +++ b/src/test/java/org/qortal/test/crosschain/apps/CheckHTLC.java @@ -0,0 +1,133 @@ +package org.qortal.test.crosschain.apps; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.LegacyAddress; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.script.Script.ScriptType; +import org.qortal.crosschain.Bitcoin; +import org.qortal.crosschain.Bitcoiny; +import org.qortal.crosschain.Litecoin; +import org.qortal.crosschain.BitcoinyHTLC; +import org.qortal.crypto.Crypto; + +import com.google.common.hash.HashCode; + +public class CheckHTLC { + + private static void usage(String error) { + if (error != null) + System.err.println(error); + + System.err.println(String.format("usage: CheckHTLC (-b | -l) ")); + System.err.println("where: -b means use Bitcoin, -l means use Litecoin"); + System.err.println(String.format("example: CheckP2SH -l " + + "2N4378NbEVGjmiUmoUD9g1vCY6kyx9tDUJ6 \\\n" + + "msAfaDaJ8JiprxxFaAXEEPxKK3JaZCYpLv \\\n" + + "\t0.00008642 \\\n" + + "\tmrBpZYYGYMwUa8tRjTiXfP1ySqNXszWN5h \\\n" + + "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n" + + "\t1600184800")); + System.exit(1); + } + + public static void main(String[] args) { + if (args.length < 7 || args.length > 7) + usage(null); + + Common.init(); + + Bitcoiny bitcoiny = null; + NetworkParameters params = null; + + Address p2shAddress = null; + Address refundAddress = null; + Coin amount = null; + Address redeemAddress = null; + byte[] secretHash = null; + int lockTime = 0; + + int argIndex = 0; + try { + switch (args[argIndex++]) { + case "-b": + bitcoiny = Bitcoin.getInstance(); + break; + + case "-l": + bitcoiny = Litecoin.getInstance(); + break; + + default: + usage("Only Bitcoin (-b) or Litecoin (-l) supported"); + } + params = bitcoiny.getNetworkParameters(); + + p2shAddress = Address.fromString(params, args[argIndex++]); + if (p2shAddress.getOutputScriptType() != ScriptType.P2SH) + usage("P2SH address invalid"); + + refundAddress = Address.fromString(params, args[argIndex++]); + if (refundAddress.getOutputScriptType() != ScriptType.P2PKH) + usage("Refund address must be in P2PKH form"); + + amount = Coin.parseCoin(args[argIndex++]); + + redeemAddress = Address.fromString(params, args[argIndex++]); + if (redeemAddress.getOutputScriptType() != ScriptType.P2PKH) + usage("Redeem address must be in P2PKH form"); + + secretHash = HashCode.fromString(args[argIndex++]).asBytes(); + if (secretHash.length != 20) + usage("Hash of secret must be 20 bytes"); + + lockTime = Integer.parseInt(args[argIndex++]); + } catch (IllegalArgumentException e) { + usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); + } + + Coin p2shFee = Coin.valueOf(Common.getP2shFee(bitcoiny)); + if (p2shFee.isZero()) + return; + + System.out.println(String.format("P2SH address: %s", p2shAddress)); + System.out.println(String.format("Refund PKH: %s", refundAddress)); + System.out.println(String.format("Redeem/refund amount: %s", amount.toPlainString())); + System.out.println(String.format("Redeem PKH: %s", redeemAddress)); + System.out.println(String.format("Hash of secret: %s", HashCode.fromBytes(secretHash))); + System.out.println(String.format("Script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime)); + + System.out.println(String.format("Redeem/refund miner's fee: %s", bitcoiny.format(p2shFee))); + + byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash); + System.out.println(String.format("Raw script bytes: %s", HashCode.fromBytes(redeemScriptBytes))); + + byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); + Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); + + if (!derivedP2shAddress.equals(p2shAddress)) { + System.err.println(String.format("Derived P2SH address %s does not match given address %s", derivedP2shAddress, p2shAddress)); + System.exit(2); + } + + amount = amount.add(p2shFee); + + // Check network's median block time + int medianBlockTime = Common.checkMedianBlockTime(bitcoiny, null); + if (medianBlockTime == 0) + return; + + // Check P2SH is funded + Common.getBalance(bitcoiny, p2shAddress.toString()); + + // Grab all unspent outputs + Common.getUnspentOutputs(bitcoiny, p2shAddress.toString()); + + Common.determineHtlcStatus(bitcoiny, p2shAddress.toString(), amount.value); + } + +} diff --git a/src/test/java/org/qortal/test/crosschain/apps/Common.java b/src/test/java/org/qortal/test/crosschain/apps/Common.java new file mode 100644 index 00000000..dfd44f2b --- /dev/null +++ b/src/test/java/org/qortal/test/crosschain/apps/Common.java @@ -0,0 +1,155 @@ +package org.qortal.test.crosschain.apps; + +import java.security.Security; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.Collections; +import java.util.List; + +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionOutput; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; +import org.qortal.crosschain.Bitcoiny; +import org.qortal.crosschain.BitcoinyHTLC; +import org.qortal.crosschain.ForeignBlockchainException; +import org.qortal.settings.Settings; + +import com.google.common.hash.HashCode; + +public abstract class Common { + + public static void init() { + Security.insertProviderAt(new BouncyCastleProvider(), 0); + Security.insertProviderAt(new BouncyCastleJsseProvider(), 1); + + Settings.fileInstance("settings-test.json"); + } + + public static long getP2shFee(Bitcoiny bitcoiny) { + long p2shFee; + + try { + p2shFee = bitcoiny.getP2shFee(null); + } catch (ForeignBlockchainException e) { + System.err.println(String.format("Unable to determine P2SH fee: %s", e.getMessage())); + return 0; + } + + return p2shFee; + } + + public static int checkMedianBlockTime(Bitcoiny bitcoiny, Integer lockTime) { + int medianBlockTime; + + try { + medianBlockTime = bitcoiny.getMedianBlockTime(); + } catch (ForeignBlockchainException e) { + System.err.println(String.format("Unable to determine median block time: %s", e.getMessage())); + return 0; + } + + System.out.println(String.format("Median block time: %s", LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC))); + + long now = System.currentTimeMillis(); + + if (now < medianBlockTime * 1000L) { + System.out.println(String.format("Too soon (%s) based on median block time %s", + LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.UTC), + LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC))); + return 0; + } + + if (lockTime != null && now < lockTime * 1000L) { + System.err.println(String.format("Too soon (%s) based on lockTime %s", + LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.UTC), + LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC))); + return 0; + } + + return medianBlockTime; + } + + public static long getBalance(Bitcoiny bitcoiny, String address58) { + long balance; + + try { + balance = bitcoiny.getConfirmedBalance(address58); + } catch (ForeignBlockchainException e) { + System.err.println(String.format("Unable to check address %s balance: %s", address58, e.getMessage())); + return 0; + } + + System.out.println(String.format("Address %s balance: %s", address58, bitcoiny.format(balance))); + + return balance; + } + + public static List getUnspentOutputs(Bitcoiny bitcoiny, String address58) { + List unspentOutputs = Collections.emptyList(); + + try { + unspentOutputs = bitcoiny.getUnspentOutputs(address58); + } catch (ForeignBlockchainException e) { + System.err.println(String.format("Can't find unspent outputs for %s: %s", address58, e.getMessage())); + return unspentOutputs; + } + + System.out.println(String.format("Found %d output%s for %s", + unspentOutputs.size(), + (unspentOutputs.size() != 1 ? "s" : ""), + address58)); + + for (TransactionOutput fundingOutput : unspentOutputs) + System.out.println(String.format("Output %s:%d amount %s", + HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), + bitcoiny.format(fundingOutput.getValue()))); + + if (unspentOutputs.isEmpty()) + System.err.println(String.format("Can't use spent/unfunded %s", address58)); + + if (unspentOutputs.size() != 1) + System.err.println(String.format("Expecting only one unspent output?")); + + return unspentOutputs; + } + + public static BitcoinyHTLC.Status determineHtlcStatus(Bitcoiny bitcoiny, String address58, long minimumAmount) { + BitcoinyHTLC.Status htlcStatus = null; + + try { + htlcStatus = BitcoinyHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), address58, minimumAmount); + + System.out.println(String.format("HTLC status: %s", htlcStatus.name())); + } catch (ForeignBlockchainException e) { + System.err.println(String.format("Unable to determine HTLC status: %s", e.getMessage())); + } + + return htlcStatus; + } + + public static void broadcastTransaction(Bitcoiny bitcoiny, Transaction transaction) { + byte[] rawTransactionBytes = transaction.bitcoinSerialize(); + + System.out.println(String.format("%nRaw transaction bytes:%n%s%n", HashCode.fromBytes(rawTransactionBytes).toString())); + + for (int countDown = 5; countDown >= 1; --countDown) { + System.out.print(String.format("\rBroadcasting transaction in %d second%s... use CTRL-C to abort ", countDown, (countDown != 1 ? "s" : ""))); + try { + Thread.sleep(1000L); + } catch (InterruptedException e) { + System.exit(0); + } + } + System.out.println("Broadcasting transaction... "); + + try { + bitcoiny.broadcastTransaction(transaction); + } catch (ForeignBlockchainException e) { + System.err.println(String.format("Failed to broadcast transaction: %s", e.getMessage())); + System.exit(1); + } + } + +} diff --git a/src/test/java/org/qortal/test/crosschain/apps/Pay.java b/src/test/java/org/qortal/test/crosschain/apps/Pay.java new file mode 100644 index 00000000..d8cf10c9 --- /dev/null +++ b/src/test/java/org/qortal/test/crosschain/apps/Pay.java @@ -0,0 +1,78 @@ +package org.qortal.test.crosschain.apps; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.Transaction; +import org.qortal.crosschain.Bitcoin; +import org.qortal.crosschain.Bitcoiny; +import org.qortal.crosschain.Litecoin; + +public class Pay { + + private static void usage(String error) { + if (error != null) + System.err.println(error); + + System.err.println(String.format("usage: Pay (-b | -l) ")); + System.err.println("where: -b means use Bitcoin, -l means use Litecoin"); + System.err.println(String.format("example: Pay -l " + + "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ \\\n" + + "\tmsAfaDaJ8JiprxxFaAXEEPxKK3JaZCYpLv \\\n" + + "\t0.00008642")); + System.exit(1); + } + + public static void main(String[] args) { + if (args.length < 4 || args.length > 4) + usage(null); + + Common.init(); + + Bitcoiny bitcoiny = null; + NetworkParameters params = null; + + String xprv58 = null; + Address address = null; + Coin amount = null; + + int argIndex = 0; + try { + switch (args[argIndex++]) { + case "-b": + bitcoiny = Bitcoin.getInstance(); + break; + + case "-l": + bitcoiny = Litecoin.getInstance(); + break; + + default: + usage("Only Bitcoin (-b) or Litecoin (-l) supported"); + } + params = bitcoiny.getNetworkParameters(); + + xprv58 = args[argIndex++]; + if (!bitcoiny.isValidXprv(xprv58)) + usage("xprv invalid"); + + address = Address.fromString(params, args[argIndex++]); + + amount = Coin.parseCoin(args[argIndex++]); + } catch (IllegalArgumentException e) { + usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); + } + + System.out.println(String.format("Address: %s", address)); + System.out.println(String.format("Amount: %s", amount.toPlainString())); + + Transaction transaction = bitcoiny.buildSpend(xprv58, address.toString(), amount.value); + if (transaction == null) { + System.err.println("Insufficent funds"); + System.exit(1); + } + + Common.broadcastTransaction(bitcoiny, transaction); + } + +} diff --git a/src/test/java/org/qortal/test/crosschain/apps/RedeemHTLC.java b/src/test/java/org/qortal/test/crosschain/apps/RedeemHTLC.java new file mode 100644 index 00000000..f79bc24d --- /dev/null +++ b/src/test/java/org/qortal/test/crosschain/apps/RedeemHTLC.java @@ -0,0 +1,164 @@ +package org.qortal.test.crosschain.apps; + +import java.util.Arrays; +import java.util.List; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.LegacyAddress; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionOutput; +import org.bitcoinj.script.Script.ScriptType; +import org.qortal.crosschain.Bitcoin; +import org.qortal.crosschain.Bitcoiny; +import org.qortal.crosschain.Litecoin; +import org.qortal.crosschain.BitcoinyHTLC; +import org.qortal.crypto.Crypto; + +import com.google.common.hash.HashCode; + +public class RedeemHTLC { + + static { + // This must go before any calls to LogManager/Logger + System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager"); + } + + private static void usage(String error) { + if (error != null) + System.err.println(error); + + System.err.println(String.format("usage: Redeem (-b | -l) ")); + System.err.println("where: -b means use Bitcoin, -l means use Litecoin"); + System.err.println(String.format("example: Redeem -l " + + "2N4378NbEVGjmiUmoUD9g1vCY6kyx9tDUJ6 \\\n" + + "\tmsAfaDaJ8JiprxxFaAXEEPxKK3JaZCYpLv \\\n" + + "\tefdaed23c4bc85c8ccae40d774af3c2a10391c648b6420cdd83cd44c27fcb5955201c64e372d \\\n" + + "\t5468697320737472696e672069732065786163746c7920333220627974657321 \\\n" + + "\t1600184800 \\\n" + + "\tmrBpZYYGYMwUa8tRjTiXfP1ySqNXszWN5h")); + System.exit(1); + } + + public static void main(String[] args) { + if (args.length < 7 || args.length > 7) + usage(null); + + Common.init(); + + Bitcoiny bitcoiny = null; + NetworkParameters params = null; + + Address p2shAddress = null; + Address refundAddress = null; + byte[] redeemPrivateKey = null; + byte[] secret = null; + int lockTime = 0; + Address outputAddress = null; + + int argIndex = 0; + try { + switch (args[argIndex++]) { + case "-b": + bitcoiny = Bitcoin.getInstance(); + break; + + case "-l": + bitcoiny = Litecoin.getInstance(); + break; + + default: + usage("Only Bitcoin (-b) or Litecoin (-l) supported"); + } + params = bitcoiny.getNetworkParameters(); + + p2shAddress = Address.fromString(params, args[argIndex++]); + if (p2shAddress.getOutputScriptType() != ScriptType.P2SH) + usage("P2SH address invalid"); + + refundAddress = Address.fromString(params, args[argIndex++]); + if (refundAddress.getOutputScriptType() != ScriptType.P2PKH) + usage("Refund address must be in P2PKH form"); + + redeemPrivateKey = HashCode.fromString(args[argIndex++]).asBytes(); + // Auto-trim + if (redeemPrivateKey.length >= 37 && redeemPrivateKey.length <= 38) + redeemPrivateKey = Arrays.copyOfRange(redeemPrivateKey, 1, 33); + if (redeemPrivateKey.length != 32) + usage("Redeem private key must be 32 bytes"); + + secret = HashCode.fromString(args[argIndex++]).asBytes(); + if (secret.length == 0) + usage("Invalid secret bytes"); + + lockTime = Integer.parseInt(args[argIndex++]); + + outputAddress = Address.fromString(params, args[argIndex++]); + if (outputAddress.getOutputScriptType() != ScriptType.P2PKH) + usage("Output address invalid"); + } catch (IllegalArgumentException e) { + usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); + } + + Coin p2shFee = Coin.valueOf(Common.getP2shFee(bitcoiny)); + if (p2shFee.isZero()) + return; + + System.out.println(String.format("Attempting to redeem HTLC %s to %s", p2shAddress, outputAddress)); + + byte[] secretHash = Crypto.hash160(secret); + + ECKey redeemKey = ECKey.fromPrivate(redeemPrivateKey); + Address redeemAddress = Address.fromKey(params, redeemKey, ScriptType.P2PKH); + + byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash); + + byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); + Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); + + if (!derivedP2shAddress.equals(p2shAddress)) { + System.err.println(String.format("Raw script bytes: %s", HashCode.fromBytes(redeemScriptBytes))); + System.err.println(String.format("Derived P2SH address %s does not match given address %s", derivedP2shAddress, p2shAddress)); + System.exit(2); + return; + } + + // Actual live processing... + + int medianBlockTime = Common.checkMedianBlockTime(bitcoiny, null); + if (medianBlockTime == 0) + return; + + // Check P2SH is funded + long p2shBalance = Common.getBalance(bitcoiny, p2shAddress.toString()); + if (p2shBalance == 0) + return; + + // Grab all unspent outputs + List unspentOutputs = Common.getUnspentOutputs(bitcoiny, p2shAddress.toString()); + if (unspentOutputs.isEmpty()) + return; + + Coin redeemAmount = Coin.valueOf(p2shBalance).subtract(p2shFee); + + BitcoinyHTLC.Status htlcStatus = Common.determineHtlcStatus(bitcoiny, p2shAddress.toString(), redeemAmount.value); + if (htlcStatus == null) + return; + + if (htlcStatus != BitcoinyHTLC.Status.FUNDED) { + System.err.println(String.format("Expecting %s HTLC status, but actual status is %s", "FUNDED", htlcStatus.name())); + System.exit(2); + return; + } + + System.out.println(String.format("Spending %s of outputs, with %s as mining fee", bitcoiny.format(redeemAmount), bitcoiny.format(p2shFee))); + + Transaction redeemTransaction = BitcoinyHTLC.buildRedeemTransaction(bitcoiny.getNetworkParameters(), redeemAmount, redeemKey, + unspentOutputs, redeemScriptBytes, secret, outputAddress.getHash()); + + Common.broadcastTransaction(bitcoiny, redeemTransaction); + } + +} diff --git a/src/test/java/org/qortal/test/crosschain/apps/RefundHTLC.java b/src/test/java/org/qortal/test/crosschain/apps/RefundHTLC.java new file mode 100644 index 00000000..cb0b0518 --- /dev/null +++ b/src/test/java/org/qortal/test/crosschain/apps/RefundHTLC.java @@ -0,0 +1,161 @@ +package org.qortal.test.crosschain.apps; + +import java.util.Arrays; +import java.util.List; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.LegacyAddress; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionOutput; +import org.bitcoinj.script.Script.ScriptType; +import org.qortal.crosschain.Litecoin; +import org.qortal.crosschain.Bitcoin; +import org.qortal.crosschain.Bitcoiny; +import org.qortal.crosschain.BitcoinyHTLC; +import org.qortal.crypto.Crypto; + +import com.google.common.hash.HashCode; + +public class RefundHTLC { + + static { + // This must go before any calls to LogManager/Logger + System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager"); + } + + private static void usage(String error) { + if (error != null) + System.err.println(error); + + System.err.println(String.format("usage: RefundHTLC (-b | -l) ")); + System.err.println("where: -b means use Bitcoin, -l means use Litecoin"); + System.err.println(String.format("example: RefundHTLC -l " + + "2N4378NbEVGjmiUmoUD9g1vCY6kyx9tDUJ6 \\\n" + + "\tef8f31b49c31b4a140aebcd9605fded88cc2dad0844c4b984f9191a5a416f72d3801e16447b0 \\\n" + + "\tmrBpZYYGYMwUa8tRjTiXfP1ySqNXszWN5h \\\n" + + "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n" + + "\t1600184800 \\\n" + + "\tmoJtbbhs7T4Z5hmBH2iyKhGrCWBzQWS2CL")); + System.exit(1); + } + + public static void main(String[] args) { + if (args.length < 7 || args.length > 7) + usage(null); + + Common.init(); + + Bitcoiny bitcoiny = null; + NetworkParameters params = null; + + Address p2shAddress = null; + byte[] refundPrivateKey = null; + Address redeemAddress = null; + byte[] secretHash = null; + int lockTime = 0; + Address outputAddress = null; + + int argIndex = 0; + try { + switch (args[argIndex++]) { + case "-b": + bitcoiny = Bitcoin.getInstance(); + break; + + case "-l": + bitcoiny = Litecoin.getInstance(); + break; + + default: + usage("Only Bitcoin (-b) or Litecoin (-l) supported"); + } + params = bitcoiny.getNetworkParameters(); + + p2shAddress = Address.fromString(params, args[argIndex++]); + if (p2shAddress.getOutputScriptType() != ScriptType.P2SH) + usage("P2SH address invalid"); + + refundPrivateKey = HashCode.fromString(args[argIndex++]).asBytes(); + // Auto-trim + if (refundPrivateKey.length >= 37 && refundPrivateKey.length <= 38) + refundPrivateKey = Arrays.copyOfRange(refundPrivateKey, 1, 33); + if (refundPrivateKey.length != 32) + usage("Refund private key must be 32 bytes"); + + redeemAddress = Address.fromString(params, args[argIndex++]); + if (redeemAddress.getOutputScriptType() != ScriptType.P2PKH) + usage("Redeem address must be in P2PKH form"); + + secretHash = HashCode.fromString(args[argIndex++]).asBytes(); + if (secretHash.length != 20) + usage("HASH160 of secret must be 20 bytes"); + + lockTime = Integer.parseInt(args[argIndex++]); + + outputAddress = Address.fromString(params, args[argIndex++]); + if (outputAddress.getOutputScriptType() != ScriptType.P2PKH) + usage("Output address invalid"); + } catch (IllegalArgumentException e) { + usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); + } + + Coin p2shFee = Coin.valueOf(Common.getP2shFee(bitcoiny)); + if (p2shFee.isZero()) + return; + + System.out.println(String.format("Attempting to refund HTLC %s to %s", p2shAddress, outputAddress)); + + ECKey refundKey = ECKey.fromPrivate(refundPrivateKey); + Address refundAddress = Address.fromKey(params, refundKey, ScriptType.P2PKH); + + byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash); + + byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); + Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); + + if (!derivedP2shAddress.equals(p2shAddress)) { + System.err.println(String.format("Raw script bytes: %s", HashCode.fromBytes(redeemScriptBytes))); + System.err.println(String.format("Derived P2SH address %s does not match given address %s", derivedP2shAddress, p2shAddress)); + System.exit(2); + } + + // Actual live processing... + + int medianBlockTime = Common.checkMedianBlockTime(bitcoiny, lockTime); + if (medianBlockTime == 0) + return; + + // Check P2SH is funded + long p2shBalance = Common.getBalance(bitcoiny, p2shAddress.toString()); + if (p2shBalance == 0) + return; + + // Grab all unspent outputs + List unspentOutputs = Common.getUnspentOutputs(bitcoiny, p2shAddress.toString()); + if (unspentOutputs.isEmpty()) + return; + + Coin refundAmount = Coin.valueOf(p2shBalance).subtract(p2shFee); + + BitcoinyHTLC.Status htlcStatus = Common.determineHtlcStatus(bitcoiny, p2shAddress.toString(), refundAmount.value); + if (htlcStatus == null) + return; + + if (htlcStatus != BitcoinyHTLC.Status.FUNDED) { + System.err.println(String.format("Expecting %s HTLC status, but actual status is %s", "FUNDED", htlcStatus.name())); + System.exit(2); + return; + } + + System.out.println(String.format("Spending %s of outputs, with %s as mining fee", bitcoiny.format(refundAmount), bitcoiny.format(p2shFee))); + + Transaction refundTransaction = BitcoinyHTLC.buildRefundTransaction(bitcoiny.getNetworkParameters(), refundAmount, refundKey, + unspentOutputs, redeemScriptBytes, lockTime, outputAddress.getHash()); + + Common.broadcastTransaction(bitcoiny, refundTransaction); + } + +} diff --git a/src/test/java/org/qortal/test/crosschain/bitcoinv1/BuildHTLC.java b/src/test/java/org/qortal/test/crosschain/bitcoinv1/BuildHTLC.java deleted file mode 100644 index 0caf030a..00000000 --- a/src/test/java/org/qortal/test/crosschain/bitcoinv1/BuildHTLC.java +++ /dev/null @@ -1,112 +0,0 @@ -package org.qortal.test.crosschain.bitcoinv1; - -import java.security.Security; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneOffset; - -import org.bitcoinj.core.Address; -import org.bitcoinj.core.Coin; -import org.bitcoinj.core.NetworkParameters; -import org.bitcoinj.script.Script.ScriptType; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; -import org.qortal.crosschain.Bitcoin; -import org.qortal.crosschain.ForeignBlockchainException; -import org.qortal.crosschain.BitcoinyHTLC; -import org.qortal.settings.Settings; - -import com.google.common.hash.HashCode; - -public class BuildHTLC { - - private static void usage(String error) { - if (error != null) - System.err.println(error); - - System.err.println(String.format("usage: BuildHTLC ")); - System.err.println(String.format("example: BuildHTLC " - + "mrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n" - + "\t0.00008642 \\\n" - + "\tn2N5VKrzq39nmuefZwp3wBiF4icdXX2B6o \\\n" - + "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n" - + "\t1585920000")); - System.exit(1); - } - - public static void main(String[] args) { - if (args.length < 5 || args.length > 5) - usage(null); - - Security.insertProviderAt(new BouncyCastleProvider(), 0); - Security.insertProviderAt(new BouncyCastleJsseProvider(), 1); - - Settings.fileInstance("settings-test.json"); - - Bitcoin bitcoin = Bitcoin.getInstance(); - NetworkParameters params = bitcoin.getNetworkParameters(); - - Address refundBitcoinAddress = null; - Coin bitcoinAmount = null; - Address redeemBitcoinAddress = null; - byte[] secretHash = null; - int lockTime = 0; - - int argIndex = 0; - try { - refundBitcoinAddress = Address.fromString(params, args[argIndex++]); - if (refundBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH) - usage("Refund BTC address must be in P2PKH form"); - - bitcoinAmount = Coin.parseCoin(args[argIndex++]); - - redeemBitcoinAddress = Address.fromString(params, args[argIndex++]); - if (redeemBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH) - usage("Redeem BTC address must be in P2PKH form"); - - secretHash = HashCode.fromString(args[argIndex++]).asBytes(); - if (secretHash.length != 20) - usage("Hash of secret must be 20 bytes"); - - lockTime = Integer.parseInt(args[argIndex++]); - int refundTimeoutDelay = lockTime - (int) (System.currentTimeMillis() / 1000L); - if (refundTimeoutDelay < 600 || refundTimeoutDelay > 30 * 24 * 60 * 60) - usage("Locktime (seconds) should be at between 10 minutes and 1 month from now"); - } catch (IllegalArgumentException e) { - usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); - } - - Coin p2shFee; - try { - p2shFee = Coin.valueOf(bitcoin.getP2shFee(null)); - } catch (ForeignBlockchainException e) { - throw new RuntimeException(e.getMessage()); - } - - System.out.println("Confirm the following is correct based on the info you've given:"); - - System.out.println(String.format("Refund Bitcoin address: %s", refundBitcoinAddress)); - System.out.println(String.format("Bitcoin redeem amount: %s", bitcoinAmount.toPlainString())); - - System.out.println(String.format("Redeem Bitcoin address: %s", redeemBitcoinAddress)); - System.out.println(String.format("Redeem miner's fee: %s", bitcoin.format(p2shFee))); - - 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 = BitcoinyHTLC.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash); - System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes))); - - String p2shAddress = bitcoin.deriveP2shAddress(redeemScriptBytes); - System.out.println(String.format("P2SH address: %s", p2shAddress)); - - bitcoinAmount = bitcoinAmount.add(p2shFee); - - // Fund P2SH - System.out.println(String.format("\nYou need to fund %s with %s (includes redeem/refund fee of %s)", - p2shAddress, bitcoin.format(bitcoinAmount), bitcoin.format(p2shFee))); - - System.out.println("Once this is done, responder should run Respond to check P2SH funding and create AT"); - } - -} diff --git a/src/test/java/org/qortal/test/crosschain/bitcoinv1/CheckHTLC.java b/src/test/java/org/qortal/test/crosschain/bitcoinv1/CheckHTLC.java deleted file mode 100644 index a8754b01..00000000 --- a/src/test/java/org/qortal/test/crosschain/bitcoinv1/CheckHTLC.java +++ /dev/null @@ -1,161 +0,0 @@ -package org.qortal.test.crosschain.bitcoinv1; - -import java.security.Security; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.util.List; - -import org.bitcoinj.core.Address; -import org.bitcoinj.core.Coin; -import org.bitcoinj.core.LegacyAddress; -import org.bitcoinj.core.NetworkParameters; -import org.bitcoinj.core.TransactionOutput; -import org.bitcoinj.script.Script.ScriptType; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; -import org.qortal.crosschain.Bitcoin; -import org.qortal.crosschain.ForeignBlockchainException; -import org.qortal.crosschain.BitcoinyHTLC; -import org.qortal.crypto.Crypto; -import org.qortal.settings.Settings; - -import com.google.common.hash.HashCode; - -public class CheckHTLC { - - private static void usage(String error) { - if (error != null) - System.err.println(error); - - System.err.println(String.format("usage: CheckHTLC ")); - System.err.println(String.format("example: CheckP2SH " - + "2NEZboTLhBDPPQciR7sExBhy3TsDi7wV3Cv \\\n" - + "mrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n" - + "\t0.00008642 \\\n" - + "\tn2N5VKrzq39nmuefZwp3wBiF4icdXX2B6o \\\n" - + "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n" - + "\t1585920000")); - System.exit(1); - } - - public static void main(String[] args) { - if (args.length < 6 || args.length > 6) - usage(null); - - Security.insertProviderAt(new BouncyCastleProvider(), 0); - Security.insertProviderAt(new BouncyCastleJsseProvider(), 1); - - Settings.fileInstance("settings-test.json"); - - Bitcoin bitcoin = Bitcoin.getInstance(); - NetworkParameters params = bitcoin.getNetworkParameters(); - - Address p2shAddress = null; - Address refundBitcoinAddress = null; - Coin bitcoinAmount = null; - Address redeemBitcoinAddress = null; - byte[] secretHash = null; - int lockTime = 0; - - int argIndex = 0; - try { - p2shAddress = Address.fromString(params, args[argIndex++]); - if (p2shAddress.getOutputScriptType() != ScriptType.P2SH) - usage("P2SH address invalid"); - - refundBitcoinAddress = Address.fromString(params, args[argIndex++]); - if (refundBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH) - usage("Refund BTC address must be in P2PKH form"); - - bitcoinAmount = Coin.parseCoin(args[argIndex++]); - - redeemBitcoinAddress = Address.fromString(params, args[argIndex++]); - if (redeemBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH) - usage("Redeem BTC address must be in P2PKH form"); - - secretHash = HashCode.fromString(args[argIndex++]).asBytes(); - if (secretHash.length != 20) - usage("Hash of secret must be 20 bytes"); - - lockTime = Integer.parseInt(args[argIndex++]); - int refundTimeoutDelay = lockTime - (int) (System.currentTimeMillis() / 1000L); - if (refundTimeoutDelay < 600 || refundTimeoutDelay > 7 * 24 * 60 * 60) - usage("Locktime (seconds) should be at between 10 minutes and 1 week from now"); - } catch (IllegalArgumentException e) { - usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); - } - - Coin p2shFee; - try { - p2shFee = Coin.valueOf(bitcoin.getP2shFee(null)); - } catch (ForeignBlockchainException e) { - throw new RuntimeException(e.getMessage()); - } - - try { - System.out.println("Confirm the following is correct based on the info you've given:"); - - System.out.println(String.format("Refund Bitcoin address: %s", redeemBitcoinAddress)); - System.out.println(String.format("Bitcoin redeem amount: %s", bitcoinAmount.toPlainString())); - - System.out.println(String.format("Redeem Bitcoin address: %s", refundBitcoinAddress)); - System.out.println(String.format("Redeem miner's fee: %s", bitcoin.format(p2shFee))); - - System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime)); - System.out.println(String.format("Hash of secret: %s", HashCode.fromBytes(secretHash))); - - System.out.println(String.format("P2SH address: %s", p2shAddress)); - - byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash); - System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes))); - - byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); - Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); - - if (!derivedP2shAddress.equals(p2shAddress)) { - System.err.println(String.format("Derived P2SH address %s does not match given address %s", derivedP2shAddress, p2shAddress)); - System.exit(2); - } - - bitcoinAmount = bitcoinAmount.add(p2shFee); - - long medianBlockTime = bitcoin.getMedianBlockTime(); - System.out.println(String.format("Median block time: %s", LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC))); - - long now = System.currentTimeMillis(); - - if (now < medianBlockTime * 1000L) - System.out.println(String.format("Too soon (%s) to redeem based on median block time %s", LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.UTC), LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC))); - - // Check P2SH is funded - long p2shBalance = bitcoin.getConfirmedBalance(p2shAddress.toString()); - System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, bitcoin.format(p2shBalance))); - - // Grab all P2SH funding transactions (just in case there are more than one) - List fundingOutputs = bitcoin.getUnspentOutputs(p2shAddress.toString()); - if (fundingOutputs == null) { - System.err.println(String.format("Can't find outputs for P2SH")); - System.exit(2); - } - - System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : ""))); - - for (TransactionOutput fundingOutput : fundingOutputs) - System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), bitcoin.format(fundingOutput.getValue()))); - - if (fundingOutputs.isEmpty()) { - System.err.println(String.format("Can't redeem spent/unfunded P2SH")); - System.exit(2); - } - - if (fundingOutputs.size() != 1) { - System.err.println(String.format("Expecting only one unspent output for P2SH")); - System.exit(2); - } - } catch (ForeignBlockchainException e) { - System.err.println("Bitcoin issue: " + e.getMessage()); - } - } - -} diff --git a/src/test/java/org/qortal/test/crosschain/bitcoinv1/Common.java b/src/test/java/org/qortal/test/crosschain/bitcoinv1/Common.java deleted file mode 100644 index 6551fd64..00000000 --- a/src/test/java/org/qortal/test/crosschain/bitcoinv1/Common.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.qortal.test.crosschain.bitcoinv1; - -import org.bitcoinj.core.Coin; - -public abstract class Common { - - public static final Coin DEFAULT_BTC_FEE = Coin.parseCoin("0.00001000"); - -} diff --git a/src/test/java/org/qortal/test/crosschain/bitcoinv1/Redeem.java b/src/test/java/org/qortal/test/crosschain/bitcoinv1/Redeem.java deleted file mode 100644 index 82cdd916..00000000 --- a/src/test/java/org/qortal/test/crosschain/bitcoinv1/Redeem.java +++ /dev/null @@ -1,203 +0,0 @@ -package org.qortal.test.crosschain.bitcoinv1; - -import java.security.Security; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.util.Arrays; -import java.util.List; - -import org.bitcoinj.core.Address; -import org.bitcoinj.core.Coin; -import org.bitcoinj.core.ECKey; -import org.bitcoinj.core.LegacyAddress; -import org.bitcoinj.core.NetworkParameters; -import org.bitcoinj.core.Transaction; -import org.bitcoinj.core.TransactionOutput; -import org.bitcoinj.script.Script.ScriptType; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; -import org.qortal.crosschain.Bitcoin; -import org.qortal.crosschain.ForeignBlockchainException; -import org.qortal.crosschain.BitcoinyHTLC; -import org.qortal.crypto.Crypto; -import org.qortal.settings.Settings; - -import com.google.common.hash.HashCode; - -public class Redeem { - - static { - // This must go before any calls to LogManager/Logger - System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager"); - } - - private static void usage(String error) { - if (error != null) - System.err.println(error); - - System.err.println(String.format("usage: Redeem ")); - System.err.println(String.format("example: Redeem " - + "2NEZboTLhBDPPQciR7sExBhy3TsDi7wV3Cv \\\n" - + "\tmrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n" - + "\tec199a4abc9d3bf024349e397535dfee9d287e174aeabae94237eb03a0118c03 \\\n" - + "\t5468697320737472696e672069732065786163746c7920333220627974657321 \\\n" - + "\t1585920000")); - System.exit(1); - } - - public static void main(String[] args) { - if (args.length < 5 || args.length > 5) - usage(null); - - Security.insertProviderAt(new BouncyCastleProvider(), 0); - Security.insertProviderAt(new BouncyCastleJsseProvider(), 1); - - Settings.fileInstance("settings-test.json"); - - Bitcoin bitcoin = Bitcoin.getInstance(); - NetworkParameters params = bitcoin.getNetworkParameters(); - - Address p2shAddress = null; - Address refundBitcoinAddress = null; - byte[] redeemPrivateKey = null; - byte[] secret = null; - int lockTime = 0; - - int argIndex = 0; - try { - p2shAddress = Address.fromString(params, args[argIndex++]); - if (p2shAddress.getOutputScriptType() != ScriptType.P2SH) - usage("P2SH address invalid"); - - refundBitcoinAddress = Address.fromString(params, args[argIndex++]); - if (refundBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH) - usage("Refund BTC address must be in P2PKH form"); - - redeemPrivateKey = HashCode.fromString(args[argIndex++]).asBytes(); - // Auto-trim - if (redeemPrivateKey.length >= 37 && redeemPrivateKey.length <= 38) - redeemPrivateKey = Arrays.copyOfRange(redeemPrivateKey, 1, 33); - if (redeemPrivateKey.length != 32) - usage("Redeem private key must be 32 bytes"); - - secret = HashCode.fromString(args[argIndex++]).asBytes(); - if (secret.length == 0) - usage("Invalid secret bytes"); - - lockTime = Integer.parseInt(args[argIndex++]); - } catch (IllegalArgumentException e) { - usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); - } - - Coin p2shFee; - try { - p2shFee = Coin.valueOf(bitcoin.getP2shFee(null)); - } catch (ForeignBlockchainException e) { - throw new RuntimeException(e.getMessage()); - } - - try { - System.out.println("Confirm the following is correct based on the info you've given:"); - - System.out.println(String.format("Redeem PRIVATE key: %s", HashCode.fromBytes(redeemPrivateKey))); - System.out.println(String.format("Redeem miner's fee: %s", bitcoin.format(p2shFee))); - System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime)); - - // New/derived info - - byte[] secretHash = Crypto.hash160(secret); - System.out.println(String.format("HASH160 of secret: %s", HashCode.fromBytes(secretHash))); - - ECKey redeemKey = ECKey.fromPrivate(redeemPrivateKey); - Address redeemAddress = Address.fromKey(params, redeemKey, ScriptType.P2PKH); - System.out.println(String.format("Redeem recipient (PKH): %s (%s)", redeemAddress, HashCode.fromBytes(redeemAddress.getHash()))); - - System.out.println(String.format("P2SH address: %s", p2shAddress)); - - byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash); - System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes))); - - byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); - Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); - - if (!derivedP2shAddress.equals(p2shAddress)) { - System.err.println(String.format("Derived P2SH address %s does not match given address %s", derivedP2shAddress, p2shAddress)); - System.exit(2); - } - - // Some checks - - System.out.println("\nProcessing:"); - - long medianBlockTime; - try { - medianBlockTime = bitcoin.getMedianBlockTime(); - } catch (ForeignBlockchainException e1) { - System.err.println("Unable to determine median block time"); - System.exit(2); - return; - } - System.out.println(String.format("Median block time: %s", LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC))); - - long now = System.currentTimeMillis(); - - if (now < medianBlockTime * 1000L) { - System.err.println(String.format("Too soon (%s) to redeem based on median block time %s", LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.UTC), LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC))); - System.exit(2); - } - - // Check P2SH is funded - long p2shBalance; - try { - p2shBalance = bitcoin.getConfirmedBalance(p2shAddress.toString()); - } catch (ForeignBlockchainException e) { - System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress)); - System.exit(2); - return; - } - System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, bitcoin.format(p2shBalance))); - - // Grab all P2SH funding transactions (just in case there are more than one) - List fundingOutputs; - try { - fundingOutputs = bitcoin.getUnspentOutputs(p2shAddress.toString()); - } catch (ForeignBlockchainException e) { - System.err.println(String.format("Can't find outputs for P2SH")); - System.exit(2); - return; - } - - System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : ""))); - - for (TransactionOutput fundingOutput : fundingOutputs) - System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), bitcoin.format(fundingOutput.getValue()))); - - if (fundingOutputs.isEmpty()) { - System.err.println(String.format("Can't redeem spent/unfunded P2SH")); - System.exit(2); - } - - if (fundingOutputs.size() != 1) { - System.err.println(String.format("Expecting only one unspent output for P2SH")); - // No longer fatal - } - - for (TransactionOutput fundingOutput : fundingOutputs) - System.out.println(String.format("Using output %s:%d for redeem", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex())); - - Coin redeemAmount = Coin.valueOf(p2shBalance).subtract(p2shFee); - System.out.println(String.format("Spending %s of output, with %s as mining fee", bitcoin.format(redeemAmount), bitcoin.format(p2shFee))); - - Transaction redeemTransaction = BitcoinyHTLC.buildRedeemTransaction(bitcoin.getNetworkParameters(), redeemAmount, redeemKey, - fundingOutputs, redeemScriptBytes, secret, redeemAddress.getHash()); - - byte[] redeemBytes = redeemTransaction.bitcoinSerialize(); - - System.out.println(String.format("\nLoad this transaction into your wallet and broadcast:\n%s\n", HashCode.fromBytes(redeemBytes).toString())); - } catch (NumberFormatException e) { - usage(String.format("Number format exception: %s", e.getMessage())); - } - } - -} diff --git a/src/test/java/org/qortal/test/crosschain/bitcoinv1/Refund.java b/src/test/java/org/qortal/test/crosschain/bitcoinv1/Refund.java deleted file mode 100644 index ab0f5966..00000000 --- a/src/test/java/org/qortal/test/crosschain/bitcoinv1/Refund.java +++ /dev/null @@ -1,207 +0,0 @@ -package org.qortal.test.crosschain.bitcoinv1; - -import java.security.Security; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.util.Arrays; -import java.util.List; - -import org.bitcoinj.core.Address; -import org.bitcoinj.core.Coin; -import org.bitcoinj.core.ECKey; -import org.bitcoinj.core.LegacyAddress; -import org.bitcoinj.core.NetworkParameters; -import org.bitcoinj.core.Transaction; -import org.bitcoinj.core.TransactionOutput; -import org.bitcoinj.script.Script.ScriptType; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; -import org.qortal.crosschain.Bitcoin; -import org.qortal.crosschain.ForeignBlockchainException; -import org.qortal.crosschain.BitcoinyHTLC; -import org.qortal.crypto.Crypto; -import org.qortal.settings.Settings; - -import com.google.common.hash.HashCode; - -public class Refund { - - static { - // This must go before any calls to LogManager/Logger - System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager"); - } - - private static void usage(String error) { - if (error != null) - System.err.println(error); - - System.err.println(String.format("usage: Refund ")); - System.err.println(String.format("example: Refund " - + "2NEZboTLhBDPPQciR7sExBhy3TsDi7wV3Cv \\\n" - + "\tef027fb5828c5e201eaf6de4cd3b0b340d16a191ef848cd691f35ef8f727358c9c01b576fb7e \\\n" - + "\tn2N5VKrzq39nmuefZwp3wBiF4icdXX2B6o \\\n" - + "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n" - + "\t1585920000")); - System.exit(1); - } - - public static void main(String[] args) { - if (args.length < 5 || args.length > 5) - usage(null); - - Security.insertProviderAt(new BouncyCastleProvider(), 0); - Security.insertProviderAt(new BouncyCastleJsseProvider(), 1); - - Settings.fileInstance("settings-test.json"); - - Bitcoin bitcoin = Bitcoin.getInstance(); - NetworkParameters params = bitcoin.getNetworkParameters(); - - Address p2shAddress = null; - byte[] refundPrivateKey = null; - Address redeemBitcoinAddress = null; - byte[] secretHash = null; - int lockTime = 0; - - int argIndex = 0; - try { - p2shAddress = Address.fromString(params, args[argIndex++]); - if (p2shAddress.getOutputScriptType() != ScriptType.P2SH) - usage("P2SH address invalid"); - - refundPrivateKey = HashCode.fromString(args[argIndex++]).asBytes(); - // Auto-trim - if (refundPrivateKey.length >= 37 && refundPrivateKey.length <= 38) - refundPrivateKey = Arrays.copyOfRange(refundPrivateKey, 1, 33); - if (refundPrivateKey.length != 32) - usage("Refund private key must be 32 bytes"); - - redeemBitcoinAddress = Address.fromString(params, args[argIndex++]); - if (redeemBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH) - usage("Their BTC address must be in P2PKH form"); - - secretHash = HashCode.fromString(args[argIndex++]).asBytes(); - if (secretHash.length != 20) - usage("HASH160 of secret must be 20 bytes"); - - lockTime = Integer.parseInt(args[argIndex++]); - } catch (IllegalArgumentException e) { - usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); - } - - Coin p2shFee; - try { - p2shFee = Coin.valueOf(bitcoin.getP2shFee(null)); - } catch (ForeignBlockchainException e) { - throw new RuntimeException(e.getMessage()); - } - - try { - System.out.println("Confirm the following is correct based on the info you've given:"); - - System.out.println(String.format("Refund PRIVATE key: %s", HashCode.fromBytes(refundPrivateKey))); - System.out.println(String.format("Redeem Bitcoin address: %s", redeemBitcoinAddress)); - System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime)); - System.out.println(String.format("P2SH address: %s", p2shAddress)); - System.out.println(String.format("Refund miner's fee: %s", bitcoin.format(p2shFee))); - - // New/derived info - - System.out.println("\nCHECKING info from other party:"); - - ECKey refundKey = ECKey.fromPrivate(refundPrivateKey); - Address refundAddress = Address.fromKey(params, refundKey, ScriptType.P2PKH); - System.out.println(String.format("Refund recipient (PKH): %s (%s)", refundAddress, HashCode.fromBytes(refundAddress.getHash()))); - - byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash); - System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes))); - - byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); - Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); - - if (!derivedP2shAddress.equals(p2shAddress)) { - System.err.println(String.format("Derived P2SH address %s does not match given address %s", derivedP2shAddress, p2shAddress)); - System.exit(2); - } - - // Some checks - - System.out.println("\nProcessing:"); - - long medianBlockTime; - try { - medianBlockTime = bitcoin.getMedianBlockTime(); - } catch (ForeignBlockchainException e) { - System.err.println("Unable to determine median block time"); - System.exit(2); - return; - } - System.out.println(String.format("Median block time: %s", LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC))); - - long now = System.currentTimeMillis(); - - if (now < medianBlockTime * 1000L) { - System.err.println(String.format("Too soon (%s) to refund based on median block time %s", LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.UTC), LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC))); - System.exit(2); - } - - if (now < lockTime * 1000L) { - System.err.println(String.format("Too soon (%s) to refund based on lockTime %s", LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.UTC), LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC))); - System.exit(2); - } - - // Check P2SH is funded - long p2shBalance; - try { - p2shBalance = bitcoin.getConfirmedBalance(p2shAddress.toString()); - } catch (ForeignBlockchainException e) { - System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress)); - System.exit(2); - return; - } - System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, bitcoin.format(p2shBalance))); - - // Grab all P2SH funding transactions (just in case there are more than one) - List fundingOutputs; - try { - fundingOutputs = bitcoin.getUnspentOutputs(p2shAddress.toString()); - } catch (ForeignBlockchainException e) { - System.err.println(String.format("Can't find outputs for P2SH")); - System.exit(2); - return; - } - - System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : ""))); - - for (TransactionOutput fundingOutput : fundingOutputs) - System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), bitcoin.format(fundingOutput.getValue()))); - - if (fundingOutputs.isEmpty()) { - System.err.println(String.format("Can't refund spent/unfunded P2SH")); - System.exit(2); - } - - if (fundingOutputs.size() != 1) { - System.err.println(String.format("Expecting only one unspent output for P2SH")); - // No longer fatal - } - - for (TransactionOutput fundingOutput : fundingOutputs) - System.out.println(String.format("Using output %s:%d for redeem", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex())); - - Coin refundAmount = Coin.valueOf(p2shBalance).subtract(p2shFee); - System.out.println(String.format("Spending %s of output, with %s as mining fee", bitcoin.format(refundAmount), bitcoin.format(p2shFee))); - - Transaction redeemTransaction = BitcoinyHTLC.buildRefundTransaction(bitcoin.getNetworkParameters(), refundAmount, refundKey, - fundingOutputs, redeemScriptBytes, lockTime, refundKey.getPubKeyHash()); - - byte[] redeemBytes = redeemTransaction.bitcoinSerialize(); - - System.out.println(String.format("\nLoad this transaction into your wallet and broadcast:\n%s\n", HashCode.fromBytes(redeemBytes).toString())); - } catch (NumberFormatException e) { - usage(String.format("Number format exception: %s", e.getMessage())); - } - } - -} diff --git a/src/test/java/org/qortal/test/crosschain/litecoinv1/BuildHTLC.java b/src/test/java/org/qortal/test/crosschain/litecoinv1/BuildHTLC.java deleted file mode 100644 index 021544ed..00000000 --- a/src/test/java/org/qortal/test/crosschain/litecoinv1/BuildHTLC.java +++ /dev/null @@ -1,112 +0,0 @@ -package org.qortal.test.crosschain.litecoinv1; - -import java.security.Security; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneOffset; - -import org.bitcoinj.core.Address; -import org.bitcoinj.core.Coin; -import org.bitcoinj.core.NetworkParameters; -import org.bitcoinj.script.Script.ScriptType; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; -import org.qortal.crosschain.ForeignBlockchainException; -import org.qortal.crosschain.Litecoin; -import org.qortal.crosschain.BitcoinyHTLC; -import org.qortal.settings.Settings; - -import com.google.common.hash.HashCode; - -public class BuildHTLC { - - private static void usage(String error) { - if (error != null) - System.err.println(error); - - System.err.println(String.format("usage: BuildHTLC ")); - System.err.println(String.format("example: BuildHTLC " - + "msAfaDaJ8JiprxxFaAXEEPxKK3JaZCYpLv \\\n" - + "\t0.00008642 \\\n" - + "\tmrBpZYYGYMwUa8tRjTiXfP1ySqNXszWN5h \\\n" - + "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n" - + "\t1600000000")); - System.exit(1); - } - - public static void main(String[] args) { - if (args.length < 5 || args.length > 5) - usage(null); - - Security.insertProviderAt(new BouncyCastleProvider(), 0); - Security.insertProviderAt(new BouncyCastleJsseProvider(), 1); - - Settings.fileInstance("settings-test.json"); - - Litecoin litecoin = Litecoin.getInstance(); - NetworkParameters params = litecoin.getNetworkParameters(); - - Address refundLitecoinAddress = null; - Coin litecoinAmount = null; - Address redeemLitecoinAddress = null; - byte[] secretHash = null; - int lockTime = 0; - - int argIndex = 0; - try { - refundLitecoinAddress = Address.fromString(params, args[argIndex++]); - if (refundLitecoinAddress.getOutputScriptType() != ScriptType.P2PKH) - usage("Refund Litecoin address must be in P2PKH form"); - - litecoinAmount = Coin.parseCoin(args[argIndex++]); - - redeemLitecoinAddress = Address.fromString(params, args[argIndex++]); - if (redeemLitecoinAddress.getOutputScriptType() != ScriptType.P2PKH) - usage("Redeem Litecoin address must be in P2PKH form"); - - secretHash = HashCode.fromString(args[argIndex++]).asBytes(); - if (secretHash.length != 20) - usage("Hash of secret must be 20 bytes"); - - lockTime = Integer.parseInt(args[argIndex++]); - int refundTimeoutDelay = lockTime - (int) (System.currentTimeMillis() / 1000L); - if (refundTimeoutDelay < 600 || refundTimeoutDelay > 30 * 24 * 60 * 60) - usage("Locktime (seconds) should be at between 10 minutes and 1 month from now"); - } catch (IllegalArgumentException e) { - usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); - } - - Coin p2shFee; - try { - p2shFee = Coin.valueOf(litecoin.getP2shFee(null)); - } catch (ForeignBlockchainException e) { - throw new RuntimeException(e.getMessage()); - } - - System.out.println("Confirm the following is correct based on the info you've given:"); - - System.out.println(String.format("Refund Litecoin address: %s", refundLitecoinAddress)); - System.out.println(String.format("Litecoin redeem amount: %s", litecoinAmount.toPlainString())); - - System.out.println(String.format("Redeem Litecoin address: %s", redeemLitecoinAddress)); - System.out.println(String.format("Redeem miner's fee: %s", litecoin.format(p2shFee))); - - 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 = BitcoinyHTLC.buildScript(refundLitecoinAddress.getHash(), lockTime, redeemLitecoinAddress.getHash(), secretHash); - System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes))); - - String p2shAddress = litecoin.deriveP2shAddress(redeemScriptBytes); - System.out.println(String.format("P2SH address: %s", p2shAddress)); - - litecoinAmount = litecoinAmount.add(p2shFee); - - // Fund P2SH - System.out.println(String.format("\nYou need to fund %s with %s (includes redeem/refund fee of %s)", - p2shAddress, litecoin.format(litecoinAmount), litecoin.format(p2shFee))); - - System.out.println("Once this is done, responder should run Respond to check P2SH funding and create AT"); - } - -} diff --git a/src/test/java/org/qortal/test/crosschain/litecoinv1/Refund.java b/src/test/java/org/qortal/test/crosschain/litecoinv1/Refund.java deleted file mode 100644 index 37752e79..00000000 --- a/src/test/java/org/qortal/test/crosschain/litecoinv1/Refund.java +++ /dev/null @@ -1,218 +0,0 @@ -package org.qortal.test.crosschain.litecoinv1; - -import java.security.Security; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.util.Arrays; -import java.util.List; - -import org.bitcoinj.core.Address; -import org.bitcoinj.core.Coin; -import org.bitcoinj.core.ECKey; -import org.bitcoinj.core.LegacyAddress; -import org.bitcoinj.core.NetworkParameters; -import org.bitcoinj.core.Transaction; -import org.bitcoinj.core.TransactionOutput; -import org.bitcoinj.script.Script.ScriptType; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; -import org.qortal.crosschain.ForeignBlockchainException; -import org.qortal.crosschain.Litecoin; -import org.qortal.crosschain.Bitcoiny; -import org.qortal.crosschain.BitcoinyHTLC; -import org.qortal.crypto.Crypto; -import org.qortal.settings.Settings; - -import com.google.common.hash.HashCode; - -public class Refund { - - static { - // This must go before any calls to LogManager/Logger - System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager"); - } - - private static void usage(String error) { - if (error != null) - System.err.println(error); - - System.err.println(String.format("usage: Refund ")); - System.err.println(String.format("example: Refund " - + "2N4378NbEVGjmiUmoUD9g1vCY6kyx9tDUJ6 \\\n" - + "\tef8f31b49c31b4a140aebcd9605fded88cc2dad0844c4b984f9191a5a416f72d3801e16447b0 \\\n" - + "\tmrBpZYYGYMwUa8tRjTiXfP1ySqNXszWN5h \\\n" - + "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n" - + "\t1600184800 \\\n" - + "\tmoJtbbhs7T4Z5hmBH2iyKhGrCWBzQWS2CL")); - System.exit(1); - } - - public static void main(String[] args) { - if (args.length < 6 || args.length > 6) - usage(null); - - Security.insertProviderAt(new BouncyCastleProvider(), 0); - Security.insertProviderAt(new BouncyCastleJsseProvider(), 1); - - Settings.fileInstance("settings-test.json"); - - Bitcoiny bitcoiny = Litecoin.getInstance(); - NetworkParameters params = bitcoiny.getNetworkParameters(); - - Address p2shAddress = null; - byte[] refundPrivateKey = null; - Address redeemAddress = null; - byte[] secretHash = null; - int lockTime = 0; - Address outputAddress = null; - - int argIndex = 0; - try { - p2shAddress = Address.fromString(params, args[argIndex++]); - if (p2shAddress.getOutputScriptType() != ScriptType.P2SH) - usage("P2SH address invalid"); - - refundPrivateKey = HashCode.fromString(args[argIndex++]).asBytes(); - // Auto-trim - if (refundPrivateKey.length >= 37 && refundPrivateKey.length <= 38) - refundPrivateKey = Arrays.copyOfRange(refundPrivateKey, 1, 33); - if (refundPrivateKey.length != 32) - usage("Refund private key must be 32 bytes"); - - redeemAddress = Address.fromString(params, args[argIndex++]); - if (redeemAddress.getOutputScriptType() != ScriptType.P2PKH) - usage("Redeem address must be in P2PKH form"); - - secretHash = HashCode.fromString(args[argIndex++]).asBytes(); - if (secretHash.length != 20) - usage("HASH160 of secret must be 20 bytes"); - - lockTime = Integer.parseInt(args[argIndex++]); - - outputAddress = Address.fromString(params, args[argIndex++]); - if (outputAddress.getOutputScriptType() != ScriptType.P2PKH) - usage("output address invalid"); - } catch (IllegalArgumentException e) { - usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); - } - - Coin p2shFee; - try { - p2shFee = Coin.valueOf(bitcoiny.getP2shFee(null)); - } catch (ForeignBlockchainException e) { - throw new RuntimeException(e.getMessage()); - } - - try { - System.out.println(String.format("P2SH address: %s", p2shAddress)); - System.out.println(String.format("Refund PRIVATE key: %s", HashCode.fromBytes(refundPrivateKey))); - System.out.println(String.format("Redeem address: %s", redeemAddress)); - System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime)); - System.out.println(String.format("Refund recipient (PKH): %s (%s)", outputAddress, HashCode.fromBytes(outputAddress.getHash()))); - - System.out.println(String.format("Refund miner's fee: %s", bitcoiny.format(p2shFee))); - - // New/derived info - - ECKey refundKey = ECKey.fromPrivate(refundPrivateKey); - Address refundAddress = Address.fromKey(params, refundKey, ScriptType.P2PKH); - - byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash); - System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes))); - - byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); - Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); - - if (!derivedP2shAddress.equals(p2shAddress)) { - System.err.println(String.format("Derived P2SH address %s does not match given address %s", derivedP2shAddress, p2shAddress)); - System.exit(2); - } - - // Some checks - - System.out.println("\nProcessing:"); - - long medianBlockTime; - try { - medianBlockTime = bitcoiny.getMedianBlockTime(); - } catch (ForeignBlockchainException e) { - System.err.println("Unable to determine median block time"); - System.exit(2); - return; - } - System.out.println(String.format("Median block time: %s", LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC))); - - long now = System.currentTimeMillis(); - - if (now < medianBlockTime * 1000L) { - System.err.println(String.format("Too soon (%s) to refund based on median block time %s", LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.UTC), LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC))); - System.exit(2); - } - - if (now < lockTime * 1000L) { - System.err.println(String.format("Too soon (%s) to refund based on lockTime %s", LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.UTC), LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC))); - System.exit(2); - } - - // Check P2SH is funded - long p2shBalance; - try { - p2shBalance = bitcoiny.getConfirmedBalance(p2shAddress.toString()); - } catch (ForeignBlockchainException e) { - System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress)); - System.exit(2); - return; - } - System.out.println(String.format("P2SH address %s balance: %s", p2shAddress, bitcoiny.format(p2shBalance))); - - // Grab all P2SH funding transactions (just in case there are more than one) - List fundingOutputs; - try { - fundingOutputs = bitcoiny.getUnspentOutputs(p2shAddress.toString()); - } catch (ForeignBlockchainException e) { - System.err.println(String.format("Can't find outputs for P2SH")); - System.exit(2); - return; - } - - System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : ""))); - - for (TransactionOutput fundingOutput : fundingOutputs) - System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), bitcoiny.format(fundingOutput.getValue()))); - - if (fundingOutputs.isEmpty()) { - System.err.println(String.format("Can't refund spent/unfunded P2SH")); - System.exit(2); - } - - if (fundingOutputs.size() != 1) { - System.err.println(String.format("Expecting only one unspent output for P2SH")); - // No longer fatal - } - - for (TransactionOutput fundingOutput : fundingOutputs) - System.out.println(String.format("Using output %s:%d for redeem", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex())); - - Coin refundAmount = Coin.valueOf(p2shBalance).subtract(p2shFee); - System.out.println(String.format("Spending %s of output, with %s as mining fee", bitcoiny.format(refundAmount), bitcoiny.format(p2shFee))); - - Transaction refundTransaction = BitcoinyHTLC.buildRefundTransaction(bitcoiny.getNetworkParameters(), refundAmount, refundKey, - fundingOutputs, redeemScriptBytes, lockTime, outputAddress.getHash()); - - byte[] rawTransactionBytes = refundTransaction.bitcoinSerialize(); - - System.out.println(String.format("\nRaw transaction bytes:\n%s\n", HashCode.fromBytes(rawTransactionBytes).toString())); - - try { - bitcoiny.broadcastTransaction(refundTransaction); - } catch (ForeignBlockchainException e) { - System.err.println(String.format("Failed to broadcast transaction: %s", e.getMessage())); - System.exit(1); - } - } catch (NumberFormatException e) { - usage(String.format("Number format exception: %s", e.getMessage())); - } - } - -} diff --git a/src/test/java/org/qortal/test/crosschain/litecoinv1/SendLTC.java b/src/test/java/org/qortal/test/crosschain/litecoinv1/SendLTC.java deleted file mode 100644 index 6e3a489a..00000000 --- a/src/test/java/org/qortal/test/crosschain/litecoinv1/SendLTC.java +++ /dev/null @@ -1,74 +0,0 @@ -package org.qortal.test.crosschain.litecoinv1; - -import java.security.Security; - -import org.bitcoinj.core.Address; -import org.bitcoinj.core.Coin; -import org.bitcoinj.core.NetworkParameters; -import org.bitcoinj.core.Transaction; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; -import org.qortal.crosschain.ForeignBlockchainException; -import org.qortal.crosschain.Litecoin; -import org.qortal.settings.Settings; - -public class SendLTC { - - private static void usage(String error) { - if (error != null) - System.err.println(error); - - System.err.println(String.format("usage: SendLTC ")); - System.err.println(String.format("example: SendLTC " - + "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ \\\n" - + "\tmsAfaDaJ8JiprxxFaAXEEPxKK3JaZCYpLv \\\n" - + "\t0.00008642")); - System.exit(1); - } - - public static void main(String[] args) { - if (args.length < 3 || args.length > 3) - usage(null); - - Security.insertProviderAt(new BouncyCastleProvider(), 0); - Security.insertProviderAt(new BouncyCastleJsseProvider(), 1); - - Settings.fileInstance("settings-test.json"); - - Litecoin litecoin = Litecoin.getInstance(); - NetworkParameters params = litecoin.getNetworkParameters(); - - String xprv58 = null; - Address litecoinAddress = null; - Coin litecoinAmount = null; - - int argIndex = 0; - try { - xprv58 = args[argIndex++]; - if (!litecoin.isValidXprv(xprv58)) - usage("xprv invalid"); - - litecoinAddress = Address.fromString(params, args[argIndex++]); - - litecoinAmount = Coin.parseCoin(args[argIndex++]); - } catch (IllegalArgumentException e) { - usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); - } - - System.out.println(String.format("Litecoin address: %s", litecoinAddress)); - System.out.println(String.format("Litecoin amount: %s", litecoinAmount.toPlainString())); - - Transaction transaction = litecoin.buildSpend(xprv58, litecoinAddress.toString(), litecoinAmount.value); - if (transaction == null) { - System.err.println("Insufficent funds"); - System.exit(1); - } - - try { - litecoin.broadcastTransaction(transaction); - } catch (ForeignBlockchainException e) { - System.err.println("Transaction broadcast failed: " + e.getMessage()); - } - } - -}