diff --git a/src/main/java/org/qora/crosschain/BTCACCT.java b/src/main/java/org/qora/crosschain/BTCACCT.java index f6670d44..76a68ee6 100644 --- a/src/main/java/org/qora/crosschain/BTCACCT.java +++ b/src/main/java/org/qora/crosschain/BTCACCT.java @@ -3,7 +3,6 @@ package org.qora.crosschain; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import org.bitcoinj.core.Address; import org.bitcoinj.core.Coin; import org.bitcoinj.core.ECKey; import org.bitcoinj.core.NetworkParameters; @@ -15,7 +14,6 @@ import org.bitcoinj.crypto.TransactionSignature; import org.bitcoinj.script.ScriptBuilder; import org.bitcoinj.script.ScriptChunk; import org.bitcoinj.script.ScriptOpCodes; -import org.bitcoinj.script.Script.ScriptType; import org.ciyam.at.FunctionCode; import org.ciyam.at.MachineState; import org.ciyam.at.OpCode; @@ -28,54 +26,48 @@ public class BTCACCT { public static final Coin DEFAULT_BTC_FEE = Coin.valueOf(1000L); // 0.00001000 BTC - private static final byte[] redeemScript1 = HashCode.fromString("76a820").asBytes(); // OP_DUP OP_SHA256 push(0x20 bytes) - private static final byte[] redeemScript2 = HashCode.fromString("87637576a914").asBytes(); // OP_EQUAL OP_IF OP_DROP OP_DUP OP_HASH160 push(0x14 bytes) - private static final byte[] redeemScript3 = HashCode.fromString("88ac6704").asBytes(); // OP_EQUALVERIFY OP_CHECKSIG OP_ELSE push(0x4 bytes) - private static final byte[] redeemScript4 = HashCode.fromString("b17576a914").asBytes(); // OP_CHECKLOCKTIMEVERIFY OP_DROP OP_DUP OP_HASH160 push(0x14 bytes) - private static final byte[] redeemScript5 = HashCode.fromString("88ac68").asBytes(); // OP_EQUALVERIFY OP_CHECKSIG OP_ENDIF + private static final byte[] redeemScript1 = HashCode.fromString("76a914").asBytes(); // OP_DUP OP_HASH160 push(0x14 bytes) + private static final byte[] redeemScript2 = HashCode.fromString("88ada97614").asBytes(); // OP_EQUALVERIFY OP_CHECKSIGVERIFY OP_HASH160 OP_DUP push(0x14 bytes) + private static final byte[] redeemScript3 = HashCode.fromString("87637504").asBytes(); // OP_EQUAL OP_IF OP_DROP push(0x4 bytes) + private static final byte[] redeemScript4 = HashCode.fromString("b16714").asBytes(); // OP_CHECKLOCKTIMEVERIFY OP_ELSE push(0x14 bytes) + private static final byte[] redeemScript5 = HashCode.fromString("8768").asBytes(); // OP_EQUAL OP_ENDIF /** * Returns Bitcoin redeem script. *

*

-	 * OP_DUP OP_SHA256 push(0x20) <SHA256 of secret> OP_EQUAL
+	 * OP_DUP OP_HASH160 push(0x14) <trade pubkeyhash> OP_EQUALVERIFY OP_CHECKSIGVERIFY
+	 * OP_HASH160 OP_DUP push(0x14) <sender/refund P2PKH> OP_EQUAL
 	 * OP_IF
-	 * 	OP_DROP OP_DUP OP_HASH160 push(0x14) <HASH160 of recipient pubkey>
-	 *	OP_EQUALVERIFY OP_CHECKSIG
+	 * 	OP_DROP push(0x04 bytes) <refund locktime> OP_CHECKLOCKTIMEVERIFY
 	 * OP_ELSE
-	 * 	push(0x04) <refund locktime> OP_CHECKLOCKTIMEVERIFY
-	 *	OP_DROP OP_DUP OP_HASH160 push(0x14) <HASH160 of sender pubkey>
-	 *	OP_EQUALVERIFY OP_CHECKSIG
+	 *	push(0x14) <redeemer P2PKH> OP_EQUAL
 	 * OP_ENDIF
 	 * 
* - * @param secretHash + * @param tradePubKeyHash * @param senderPubKey * @param recipientPubKey * @param lockTime * @return */ - public static byte[] buildRedeemScript(byte[] secretHash, byte[] senderPubKey, byte[] recipientPubKey, int lockTime) { - byte[] senderPubKeyHash160 = BTC.hash160(senderPubKey); - byte[] recipientPubKeyHash160 = BTC.hash160(recipientPubKey); - - return Bytes.concat(redeemScript1, secretHash, redeemScript2, recipientPubKeyHash160, redeemScript3, BitTwiddling.toLEByteArray((int) (lockTime & 0xffffffffL)), - redeemScript4, senderPubKeyHash160, redeemScript5); + public static byte[] buildScript(byte[] tradePubKeyHash, byte[] senderPubKeyHash, byte[] recipientPubKeyHash, int lockTime) { + return Bytes.concat(redeemScript1, tradePubKeyHash, redeemScript2, senderPubKeyHash, redeemScript3, BitTwiddling.toLEByteArray((int) (lockTime & 0xffffffffL)), + redeemScript4, recipientPubKeyHash, redeemScript5); } - public static Transaction buildRefundTransaction(Coin refundAmount, ECKey senderKey, TransactionOutput fundingOutput, byte[] redeemScriptBytes, long lockTime) { + public static Transaction buildRefundTransaction(Coin refundAmount, ECKey tradeKey, byte[] senderPubKey, TransactionOutput fundingOutput, byte[] redeemScriptBytes, long lockTime) { NetworkParameters params = BTC.getInstance().getNetworkParameters(); Transaction refundTransaction = new Transaction(params); refundTransaction.setVersion(2); - refundAmount = refundAmount.subtract(DEFAULT_BTC_FEE); - // Output is back to P2SH funder - refundTransaction.addOutput(refundAmount, ScriptBuilder.createOutputScript(Address.fromKey(params, senderKey, ScriptType.P2PKH))); + ECKey senderKey = ECKey.fromPublicOnly(senderPubKey); + refundTransaction.addOutput(refundAmount, ScriptBuilder.createP2PKHOutputScript(senderKey)); // Input (without scriptSig prior to signing) - TransactionInput input = new TransactionInput(params, null, new byte[0], fundingOutput.getOutPointFor()); + TransactionInput input = new TransactionInput(params, null, redeemScriptBytes, fundingOutput.getOutPointFor()); input.setSequenceNumber(0); // Use 0, not max-value, so lockTime can be used refundTransaction.addInput(input); @@ -84,27 +76,73 @@ public class BTCACCT { // Generate transaction signature for input final boolean anyoneCanPay = false; - TransactionSignature txSig = refundTransaction.calculateSignature(0, senderKey, redeemScriptBytes, SigHash.ALL, anyoneCanPay); + TransactionSignature txSig = refundTransaction.calculateSignature(0, tradeKey, redeemScriptBytes, SigHash.ALL, anyoneCanPay); // Build scriptSig with... ScriptBuilder scriptBuilder = new ScriptBuilder(); + // sender/refund pubkey + scriptBuilder.addChunk(new ScriptChunk(senderPubKey.length, senderPubKey)); + // transaction signature byte[] txSigBytes = txSig.encodeToBitcoin(); scriptBuilder.addChunk(new ScriptChunk(txSigBytes.length, txSigBytes)); - // sender's public key - byte[] senderPubKey = senderKey.getPubKey(); - scriptBuilder.addChunk(new ScriptChunk(senderPubKey.length, senderPubKey)); + // trade public key + byte[] tradePubKey = tradeKey.getPubKey(); + scriptBuilder.addChunk(new ScriptChunk(tradePubKey.length, tradePubKey)); /// redeem script scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes)); + // Set input scriptSig refundTransaction.getInput(0).setScriptSig(scriptBuilder.build()); return refundTransaction; } + public static Transaction buildRedeemTransaction(Coin redeemAmount, ECKey tradeKey, byte[] recipientPubKey, TransactionOutput fundingOutput, byte[] redeemScriptBytes) { + NetworkParameters params = BTC.getInstance().getNetworkParameters(); + + Transaction redeemTransaction = new Transaction(params); + redeemTransaction.setVersion(2); + + // Output to redeem recipient + ECKey senderKey = ECKey.fromPublicOnly(recipientPubKey); + redeemTransaction.addOutput(redeemAmount, ScriptBuilder.createP2PKHOutputScript(senderKey)); + + // Input (without scriptSig prior to signing) + TransactionInput input = new TransactionInput(params, null, redeemScriptBytes, fundingOutput.getOutPointFor()); + input.setSequenceNumber(0); // Use 0, not max-value, so lockTime can be used + redeemTransaction.addInput(input); + + // Generate transaction signature for input + final boolean anyoneCanPay = false; + TransactionSignature txSig = redeemTransaction.calculateSignature(0, tradeKey, redeemScriptBytes, SigHash.ALL, anyoneCanPay); + + // Build scriptSig with... + ScriptBuilder scriptBuilder = new ScriptBuilder(); + + // recipient pubkey + scriptBuilder.addChunk(new ScriptChunk(recipientPubKey.length, recipientPubKey)); + + // transaction signature + byte[] txSigBytes = txSig.encodeToBitcoin(); + scriptBuilder.addChunk(new ScriptChunk(txSigBytes.length, txSigBytes)); + + // trade public key + byte[] tradePubKey = tradeKey.getPubKey(); + scriptBuilder.addChunk(new ScriptChunk(tradePubKey.length, tradePubKey)); + + /// redeem script + scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes)); + + // Set input scriptSig + redeemTransaction.getInput(0).setScriptSig(scriptBuilder.build()); + + return redeemTransaction; + } + public static byte[] buildCiyamAT(byte[] secretHash, byte[] destinationQortalPubKey, long refundMinutes) { // Labels for data segment addresses int addrCounter = 0; diff --git a/src/main/java/org/qortal/crosschain/BTC.java b/src/main/java/org/qortal/crosschain/BTC.java index d55b84f1..7eaee53b 100644 --- a/src/main/java/org/qortal/crosschain/BTC.java +++ b/src/main/java/org/qortal/crosschain/BTC.java @@ -10,6 +10,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; +import java.net.InetAddress; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.security.DigestOutputStream; @@ -28,6 +29,7 @@ import org.bitcoinj.core.BlockChain; import org.bitcoinj.core.CheckpointManager; import org.bitcoinj.core.Coin; import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.PeerAddress; import org.bitcoinj.core.PeerGroup; import org.bitcoinj.core.Sha256Hash; import org.bitcoinj.core.StoredBlock; @@ -35,6 +37,7 @@ import org.bitcoinj.core.TransactionOutput; import org.bitcoinj.core.listeners.NewBestBlockListener; import org.bitcoinj.net.discovery.DnsDiscovery; import org.bitcoinj.params.MainNetParams; +import org.bitcoinj.params.RegTestParams; import org.bitcoinj.params.TestNet3Params; import org.bitcoinj.store.BlockStore; import org.bitcoinj.store.BlockStoreException; @@ -107,7 +110,7 @@ public class BTC { return; // Too recent LOGGER.trace(() -> String.format("Checkpointing at block %d dated %s", height, LocalDateTime.ofInstant(Instant.ofEpochSecond(blockTimestamp), ZoneOffset.UTC))); - checkpoints.put(blockTimestamp, block); + this.checkpoints.put(blockTimestamp, block); try { this.saveAsText(new File(BTC.getInstance().getDirectory(), BTC.getInstance().getCheckpointsFileName())); @@ -121,11 +124,11 @@ public class BTC { try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(textFile), StandardCharsets.US_ASCII))) { writer.println("TXT CHECKPOINTS 1"); writer.println("0"); // Number of signatures to read. Do this later. - writer.println(checkpoints.size()); + writer.println(this.checkpoints.size()); ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE); - for (StoredBlock block : checkpoints.values()) { + for (StoredBlock block : this.checkpoints.values()) { block.serializeCompact(buffer); writer.println(CheckpointManager.BASE64.encode(buffer.array())); buffer.position(0); @@ -145,11 +148,11 @@ public class BTC { dataOutputStream.writeBytes("CHECKPOINTS 1"); dataOutputStream.writeInt(0); // Number of signatures to read. Do this later. digestOutputStream.on(true); - dataOutputStream.writeInt(checkpoints.size()); + dataOutputStream.writeInt(this.checkpoints.size()); ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE); - for (StoredBlock block : checkpoints.values()) { + for (StoredBlock block : this.checkpoints.values()) { block.serializeCompact(buffer); dataOutputStream.write(buffer.array()); buffer.position(0); @@ -165,8 +168,10 @@ public class BTC { private BTC() { if (Settings.getInstance().useBitcoinTestNet()) { - this.params = TestNet3Params.get(); - this.checkpointsFileName = "checkpoints-testnet.txt"; + this.params = RegTestParams.get(); + this.checkpointsFileName = "checkpoints-regtest.txt"; + // TestNet3Params.get(); + // this.checkpointsFileName = "checkpoints-testnet.txt"; } else { this.params = MainNetParams.get(); this.checkpointsFileName = "checkpoints.txt"; @@ -231,7 +236,13 @@ public class BTC { this.peerGroup = new PeerGroup(this.params, this.chain); this.peerGroup.setUserAgent("qortal", "1.0"); - this.peerGroup.addPeerDiscovery(new DnsDiscovery(this.params)); + + if (this.params != RegTestParams.get()) { + this.peerGroup.addPeerDiscovery(new DnsDiscovery(this.params)); + } else { + peerGroup.addAddress(PeerAddress.localhost(this.params)); + } + this.peerGroup.start(); } @@ -306,6 +317,7 @@ public class BTC { block = block.getPrev(this.blockStore); } + // Descending, but order shouldn't matter as we're picking median... latestBlocks.sort((a, b) -> Long.compare(b.getHeader().getTimeSeconds(), a.getHeader().getTimeSeconds())); return latestBlocks.get(5).getHeader().getTimeSeconds(); diff --git a/src/test/java/org/qora/test/apps/BuildCheckpoints.java b/src/test/java/org/qora/test/apps/BuildCheckpoints.java new file mode 100644 index 00000000..44869bb7 --- /dev/null +++ b/src/test/java/org/qora/test/apps/BuildCheckpoints.java @@ -0,0 +1,69 @@ +package org.qora.test.apps; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.TreeMap; + +import org.bitcoinj.core.BlockChain; +import org.bitcoinj.core.CheckpointManager; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.PeerAddress; +import org.bitcoinj.core.PeerGroup; +import org.bitcoinj.core.StoredBlock; +import org.bitcoinj.core.VerificationException; +import org.bitcoinj.core.listeners.NewBestBlockListener; +import org.bitcoinj.params.RegTestParams; +import org.bitcoinj.store.BlockStore; +import org.bitcoinj.store.MemoryBlockStore; + +public class BuildCheckpoints { + + private static final TreeMap checkpoints = new TreeMap<>(); + + public static void main(String[] args) throws Exception { + final NetworkParameters params = RegTestParams.get(); + + final BlockStore store = new MemoryBlockStore(params); + final BlockChain chain = new BlockChain(params, store); + final PeerGroup peerGroup = new PeerGroup(params, chain); + + final InetAddress ipAddress = InetAddress.getLocalHost(); + final PeerAddress peerAddress = new PeerAddress(params, ipAddress); + peerGroup.addAddress(peerAddress); + peerGroup.start(); + + chain.addNewBestBlockListener((block) -> checkpoints.put(block.getHeight(), block)); + + peerGroup.downloadBlockChain(); + peerGroup.stop(); + + final File checkpointsFile = new File("regtest-checkpoints"); + saveAsText(checkpointsFile); + } + + private static void saveAsText(File textFile) { + try (PrintWriter writer = new PrintWriter( + new OutputStreamWriter(new FileOutputStream(textFile), StandardCharsets.US_ASCII))) { + writer.println("TXT CHECKPOINTS 1"); + writer.println("0"); // Number of signatures to read. Do this later. + writer.println(checkpoints.size()); + + ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE); + + for (StoredBlock block : checkpoints.values()) { + block.serializeCompact(buffer); + writer.println(CheckpointManager.BASE64.encode(buffer.array())); + buffer.position(0); + } + } catch (FileNotFoundException e) { + return; + } + } + +} diff --git a/src/test/java/org/qora/test/btcacct/Initiate.java b/src/test/java/org/qora/test/btcacct/Initiate.java new file mode 100644 index 00000000..d081f4ba --- /dev/null +++ b/src/test/java/org/qora/test/btcacct/Initiate.java @@ -0,0 +1,144 @@ +package org.qora.test.btcacct; + +import java.security.Security; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.AddressFormatException; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.LegacyAddress; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.params.RegTestParams; +import org.bitcoinj.params.TestNet3Params; +import org.bitcoinj.script.Script.ScriptType; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.qora.controller.Controller; +import org.qora.crosschain.BTC; +import org.qora.crosschain.BTCACCT; +import org.qora.repository.DataException; +import org.qora.repository.Repository; +import org.qora.repository.RepositoryFactory; +import org.qora.repository.RepositoryManager; +import org.qora.repository.hsqldb.HSQLDBRepositoryFactory; +import org.qora.settings.Settings; + +import com.google.common.hash.HashCode; + +/** + * Initiator must be Qora-chain so that initiator can send initial message to BTC P2SH then Qora can scan for P2SH add send corresponding message to Qora AT. + * + * Initiator (wants Qora, has BTC) + * Funds BTC P2SH address + * + * Responder (has Qora, wants BTC) + * Builds Qora ACCT AT and funds it with Qora + * + * Initiator sends recipient+secret+script as input to BTC P2SH address, releasing BTC amount - fees to responder + * + * Qora nodes scan for P2SH output, checks amount and recipient and if ok sends secret to Qora ACCT AT + * (Or it's possible to feed BTC transaction details into Qora AT so it can check them itself?) + * + * Qora ACCT AT sends its Qora to initiator + * + */ + +public class Initiate { + + private static final long REFUND_TIMEOUT = 600L; // seconds + + private static void usage(String error) { + if (error != null) + System.err.println(error); + + System.err.println(String.format("usage: Initiate ()")); + System.err.println(String.format("example: Initiate mrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n" + + "\t0.00008642 \\\n" + + "\tn2N5VKrzq39nmuefZwp3wBiF4icdXX2B6o")); + System.exit(1); + } + + public static void main(String[] args) { + if (args.length < 3 || args.length > 4) + usage(null); + + Security.insertProviderAt(new BouncyCastleProvider(), 0); + Settings.fileInstance("settings-test.json"); + NetworkParameters params = RegTestParams.get(); + // TestNet3Params.get(); + + Address yourBitcoinAddress = null; + Coin bitcoinAmount = null; + Address theirBitcoinAddress = null; + Coin bitcoinFee = BTCACCT.DEFAULT_BTC_FEE; + + try { + int argIndex = 0; + + yourBitcoinAddress = Address.fromString(params, args[argIndex++]); + if (yourBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH) + usage("Your BTC address is not in P2PKH form"); + + bitcoinAmount = Coin.parseCoin(args[argIndex++]); + + theirBitcoinAddress = Address.fromString(params, args[argIndex++]); + if (theirBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH) + usage("Their BTC address is not in P2PKH form"); + + if (args.length > argIndex) + bitcoinFee = Coin.parseCoin(args[argIndex++]); + } catch (NumberFormatException | AddressFormatException e) { + usage(String.format("Argument format exception: %s", e.getMessage())); + } + + try { + RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl()); + RepositoryManager.setRepositoryFactory(repositoryFactory); + } catch (DataException e) { + throw new RuntimeException("Repository startup issue: " + e.getMessage()); + } + + try (final Repository repository = RepositoryManager.getRepository()) { + System.out.println("Confirm the following is correct based on the info you've given:"); + + System.out.println(String.format("Your Bitcoin address: %s", yourBitcoinAddress)); + System.out.println(String.format("Their Bitcoin address: %s", theirBitcoinAddress)); + System.out.println(String.format("Bitcoin redeem amount: %s", bitcoinAmount.toPlainString())); + System.out.println(String.format("Bitcoin redeem fee: %s", bitcoinFee.toPlainString())); + + // New/derived info + + ECKey tradeKey = new ECKey(); + System.out.println("\nSecret info (DO NOT share with other party):"); + System.out.println(String.format("Trade private key: %s", HashCode.fromBytes(tradeKey.getPrivKeyBytes()))); + + System.out.println("\nGive this info to other party:"); + + System.out.println(String.format("Trade pubkeyhash: %s", HashCode.fromBytes(tradeKey.getPubKeyHash()))); + + int lockTime = (int) ((System.currentTimeMillis() / 1000L) + REFUND_TIMEOUT); + System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime)); + + byte[] redeemScriptBytes = BTCACCT.buildScript(tradeKey.getPubKeyHash(), yourBitcoinAddress.getHash(), theirBitcoinAddress.getHash(), lockTime); + System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes))); + + byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes); + + Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); + System.out.println(String.format("P2SH address: %s", p2shAddress)); + + bitcoinAmount = bitcoinAmount.add(bitcoinFee); + + // Fund P2SH + System.out.println(String.format("\nYou need to fund %s with %s BTC (includes redeem/refund fee of %s)", + p2shAddress.toString(), bitcoinAmount.toPlainString(), bitcoinFee.toPlainString())); + + System.out.println("Once this is done, responder should run Respond to check P2SH funding and create AT"); + } catch (DataException e) { + throw new RuntimeException("Repository issue: " + e.getMessage()); + } + } + +} diff --git a/src/test/java/org/qora/test/btcacct/Initiate1.java b/src/test/java/org/qora/test/btcacct/Initiate1.java deleted file mode 100644 index 60001176..00000000 --- a/src/test/java/org/qora/test/btcacct/Initiate1.java +++ /dev/null @@ -1,155 +0,0 @@ -package org.qora.test.btcacct; - -import java.math.BigDecimal; -import java.security.SecureRandom; -import java.security.Security; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; - -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.params.TestNet3Params; -import org.bitcoinj.script.Script.ScriptType; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.qora.account.PrivateKeyAccount; -import org.qora.account.PublicKeyAccount; -import org.qora.asset.Asset; -import org.qora.controller.Controller; -import org.qora.crosschain.BTC; -import org.qora.crosschain.BTCACCT; -import org.qora.crypto.Crypto; -import org.qora.repository.DataException; -import org.qora.repository.Repository; -import org.qora.repository.RepositoryFactory; -import org.qora.repository.RepositoryManager; -import org.qora.repository.hsqldb.HSQLDBRepositoryFactory; -import org.qora.utils.Base58; - -import com.google.common.hash.HashCode; - -/** - * Initiator must be Qora-chain so that initiator can send initial message to BTC P2SH then Qora can scan for P2SH add send corresponding message to Qora AT. - * - * Initiator (wants Qora, has BTC) - * Funds BTC P2SH address - * - * Responder (has Qora, wants BTC) - * Builds Qora ACCT AT and funds it with Qora - * - * Initiator sends recipient+secret+script as input to BTC P2SH address, releasing BTC amount - fees to responder - * - * Qora nodes scan for P2SH output, checks amount and recipient and if ok sends secret to Qora ACCT AT - * (Or it's possible to feed BTC transaction details into Qora AT so it can check them itself?) - * - * Qora ACCT AT sends its Qora to initiator - * - */ - -public class Initiate1 { - - private static final long REFUND_TIMEOUT = 600L; // seconds - - private static void usage() { - System.err.println(String.format("usage: Initiate1 ")); - System.err.println(String.format("example: Initiate1 pYQ6DpQBJ2n72TCLJLScEvwhf3boxWy2kQEPynakwpj \\\n" - + "\t03aa20871c2195361f2826c7a649eab6b42639630c4d8c33c55311d5c1e476b5d6 \\\n" - + "\t123 0.00008642 \\\n" - + "\tJBNBQQDzZsm5do1BrwWAp53Ps4KYJVt749EGpCf7ofte \\\n" - + "\t032783606be32a3e639a33afe2b15f058708ab124f3b290d595ee954390a0c8559")); - System.exit(1); - } - - public static void main(String[] args) { - if (args.length != 6) - usage(); - - Security.insertProviderAt(new BouncyCastleProvider(), 0); - NetworkParameters params = TestNet3Params.get(); - - int argIndex = 0; - String yourQortPrivKey58 = args[argIndex++]; - String yourBitcoinPubKeyHex = args[argIndex++]; - - String rawQortAmount = args[argIndex++]; - String rawBitcoinAmount = args[argIndex++]; - - String theirQortPubKey58 = args[argIndex++]; - String theirBitcoinPubKeyHex = args[argIndex++]; - - try { - RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl()); - RepositoryManager.setRepositoryFactory(repositoryFactory); - } catch (DataException e) { - throw new RuntimeException("Repository startup issue: " + e.getMessage()); - } - - try (final Repository repository = RepositoryManager.getRepository()) { - System.out.println("Confirm the following is correct based on the info you've given:"); - - byte[] yourQortPrivKey = Base58.decode(yourQortPrivKey58); - PrivateKeyAccount yourQortalAccount = new PrivateKeyAccount(repository, yourQortPrivKey); - System.out.println(String.format("Your Qortal address: %s", yourQortalAccount.getAddress())); - - byte[] yourBitcoinPubKey = HashCode.fromString(yourBitcoinPubKeyHex).asBytes(); - ECKey yourBitcoinKey = ECKey.fromPublicOnly(yourBitcoinPubKey); - Address yourBitcoinAddress = Address.fromKey(params, yourBitcoinKey, ScriptType.P2PKH); - System.out.println(String.format("Your Bitcoin address: %s", yourBitcoinAddress)); - - byte[] theirQortPubKey = Base58.decode(theirQortPubKey58); - PublicKeyAccount theirQortalAccount = new PublicKeyAccount(repository, theirQortPubKey); - System.out.println(String.format("Their Qortal address: %s", theirQortalAccount.getAddress())); - - byte[] theirBitcoinPubKey = HashCode.fromString(theirBitcoinPubKeyHex).asBytes(); - ECKey theirBitcoinKey = ECKey.fromPublicOnly(theirBitcoinPubKey); - Address theirBitcoinAddress = Address.fromKey(params, theirBitcoinKey, ScriptType.P2PKH); - System.out.println(String.format("Their Bitcoin address: %s", theirBitcoinAddress)); - - // Some checks - BigDecimal qortAmount = new BigDecimal(rawQortAmount).setScale(8); - BigDecimal yourQortBalance = yourQortalAccount.getConfirmedBalance(Asset.QORT); - if (yourQortBalance.compareTo(qortAmount) <= 0) { - System.err.println(String.format("Your QORT balance %s is less than required %s", yourQortBalance.toPlainString(), qortAmount.toPlainString())); - System.exit(2); - } - - // New/derived info - - byte[] secret = new byte[32]; - new SecureRandom().nextBytes(secret); - System.out.println("\nSecret info (DO NOT share with other party):"); - System.out.println("Secret: " + HashCode.fromBytes(secret).toString()); - - System.out.println("\nGive this info to other party:"); - - byte[] secretHash = Crypto.digest(secret); - System.out.println("Hash of secret: " + HashCode.fromBytes(secretHash).toString()); - - int lockTime = (int) ((System.currentTimeMillis() / 1000L) + REFUND_TIMEOUT); - System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneId.systemDefault()), lockTime)); - - byte[] redeemScriptBytes = BTCACCT.buildRedeemScript(secretHash, yourBitcoinPubKey, theirBitcoinPubKey, lockTime); - System.out.println("Redeem script: " + HashCode.fromBytes(redeemScriptBytes).toString()); - - byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes); - - Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); - System.out.println("P2SH address: " + p2shAddress.toString()); - - Coin bitcoinAmount = Coin.parseCoin(rawBitcoinAmount).add(BTCACCT.DEFAULT_BTC_FEE); - - // Fund P2SH - System.out.println(String.format("\nYou need to fund %s with %s BTC (includes redeem/refund fee)", p2shAddress.toString(), bitcoinAmount.toPlainString())); - - System.out.println("Once this is done, responder should run Respond2 to check P2SH funding and create AT"); - } catch (NumberFormatException e) { - usage(); - } catch (DataException e) { - throw new RuntimeException("Repository issue: " + e.getMessage()); - } - } - -} diff --git a/src/test/java/org/qora/test/btcacct/Redeem.java b/src/test/java/org/qora/test/btcacct/Redeem.java new file mode 100644 index 00000000..1ba65bce --- /dev/null +++ b/src/test/java/org/qora/test/btcacct/Redeem.java @@ -0,0 +1,199 @@ +package org.qora.test.btcacct; + +import java.security.Security; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.List; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.AddressFormatException; +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.params.RegTestParams; +import org.bitcoinj.params.TestNet3Params; +import org.bitcoinj.script.Script.ScriptType; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.qora.controller.Controller; +import org.qora.crosschain.BTC; +import org.qora.crosschain.BTCACCT; +import org.qora.repository.DataException; +import org.qora.repository.Repository; +import org.qora.repository.RepositoryFactory; +import org.qora.repository.RepositoryManager; +import org.qora.repository.hsqldb.HSQLDBRepositoryFactory; +import org.qora.settings.Settings; + +import com.google.common.hash.HashCode; + +/** + * Initiator must be Qora-chain so that initiator can send initial message to BTC P2SH then Qora can scan for P2SH add send corresponding message to Qora AT. + * + * Initiator (wants Qora, has BTC) + * Funds BTC P2SH address + * + * Responder (has Qora, wants BTC) + * Builds Qora ACCT AT and funds it with Qora + * + * Initiator sends trade private key to Responder. + * Responder uses their public key + tx signature + trade pubkey + script as input to BTC P2SH address, releasing BTC amount to responder. + * + * Qora nodes scan for P2SH output, checks amount and recipient and if ok sends secret to Qora ACCT AT + * (Or it's possible to feed BTC transaction details into Qora AT so it can check them itself?) + * + * Qora ACCT AT sends its Qora to initiator + * + */ + +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 final long REFUND_TIMEOUT = 600L; // seconds + + 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 032783606be32a3e639a33afe2b15f058708ab124f3b290d595ee954390a0c8559 \\\n" + + "\tmrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n" + + "\teb95e1c1a5e9e6733549faec85b71f74f67638ea63b0acf2f077e9d0cb94dfe8 1575653814 2Mtn4aLjjWVEWckdoTMK7P8WbkXJf1ES6yL")); + System.exit(1); + } + + public static void main(String[] args) { + if (args.length < 5 || args.length > 6) + usage(null); + + Security.insertProviderAt(new BouncyCastleProvider(), 0); + Settings.fileInstance("settings-test.json"); + NetworkParameters params = RegTestParams.get(); + // TestNet3Params.get(); + + ECKey yourBitcoinKey = null; + Address theirBitcoinAddress = null; + byte[] tradePrivateKey = null; + int lockTime = 0; + Address p2shAddress = null; + Coin bitcoinFee = BTCACCT.DEFAULT_BTC_FEE; + + try { + int argIndex = 0; + + yourBitcoinKey = ECKey.fromPublicOnly(HashCode.fromString(args[argIndex++]).asBytes()); + + theirBitcoinAddress = Address.fromString(params, args[argIndex++]); + if (theirBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH) + usage("Their BTC address is not in P2PKH form"); + + tradePrivateKey = HashCode.fromString(args[argIndex++]).asBytes(); + if (tradePrivateKey.length != 32) + usage("Trade private key not 32 bytes"); + + lockTime = Integer.parseInt(args[argIndex++]); + + p2shAddress = Address.fromString(params, args[argIndex++]); + if (p2shAddress.getOutputScriptType() != ScriptType.P2SH) + usage("P2SH address invalid"); + + if (args.length > argIndex) + bitcoinFee = Coin.parseCoin(args[argIndex++]); + } catch (NumberFormatException | AddressFormatException e) { + usage(String.format("Argument format exception: %s", e.getMessage())); + } + + try { + RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl()); + RepositoryManager.setRepositoryFactory(repositoryFactory); + } catch (DataException e) { + throw new RuntimeException("Repository startup issue: " + e.getMessage()); + } + + try (final Repository repository = RepositoryManager.getRepository()) { + System.out.println("Confirm the following is correct based on the info you've given:"); + + System.out.println(String.format("Your Bitcoin address: %s", Address.fromKey(params, yourBitcoinKey, ScriptType.P2PKH))); + System.out.println(String.format("Their Bitcoin address: %s", theirBitcoinAddress)); + System.out.println(String.format("Trade PRIVATE key: %s", HashCode.fromBytes(tradePrivateKey))); + 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("Bitcoin redeem fee: %s", bitcoinFee.toPlainString())); + + // New/derived info + + System.out.println("\nCHECKING info from other party:"); + + ECKey tradeKey = ECKey.fromPrivate(tradePrivateKey); + System.out.println(String.format("Trade pubkeyhash: %s", HashCode.fromBytes(tradeKey.getPubKeyHash()))); + + byte[] redeemScriptBytes = BTCACCT.buildScript(tradeKey.getPubKeyHash(), theirBitcoinAddress.getHash(), yourBitcoinKey.getPubKeyHash(), lockTime); + System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes))); + + byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes); + Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); + + if (!derivedP2shAddress.equals(p2shAddress)) { + System.err.println(String.format("Derived P2SH address %s does not match given address %s", derivedP2shAddress, p2shAddress)); + System.exit(2); + } + + // Some checks + + System.out.println("\nProcessing:"); + + long medianBlockTime = BTC.getInstance().getMedianBlockTime(); + System.out.println(String.format("Median block time: %s", LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC))); + + long now = System.currentTimeMillis(); + + if (now < medianBlockTime * 1000L) { + System.err.println(String.format("Too soon (%s) to redeem based on median block time %s", LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.UTC), LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC))); + System.exit(2); + } + + Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString(), lockTime - REFUND_TIMEOUT); + if (p2shBalance == null) { + System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress)); + System.exit(2); + } + System.out.println(String.format("P2SH address %s balance: %s BTC", p2shAddress, p2shBalance.toPlainString())); + + // Grab all P2SH funding transactions (just in case there are more than one) + List fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString(), lockTime - REFUND_TIMEOUT); + System.out.println(String.format("Found %d unspent output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : ""))); + + if (fundingOutputs.isEmpty()) { + System.err.println(String.format("Can't redeem spent/unfunded P2SH")); + System.exit(2); + } + + if (fundingOutputs.size() != 1) { + System.err.println(String.format("Expecting only one unspent output for P2SH")); + System.exit(2); + } + + TransactionOutput fundingOutput = fundingOutputs.get(0); + System.out.println(String.format("Using output %s:%d for redeem", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex())); + + Coin redeemAmount = p2shBalance.subtract(bitcoinFee); + Transaction redeemTransaction = BTCACCT.buildRedeemTransaction(redeemAmount, tradeKey, yourBitcoinKey.getPubKey(), fundingOutput, redeemScriptBytes); + + byte[] redeemBytes = redeemTransaction.bitcoinSerialize(); + + System.out.println(String.format("\nLoad this transaction into your wallet and broadcast:\n%s\n", HashCode.fromBytes(redeemBytes).toString())); + } catch (NumberFormatException e) { + usage(String.format("Number format exception: %s", e.getMessage())); + } catch (DataException e) { + throw new RuntimeException("Repository issue: " + e.getMessage()); + } + } + +} diff --git a/src/test/java/org/qora/test/btcacct/Refund2.java b/src/test/java/org/qora/test/btcacct/Refund.java similarity index 57% rename from src/test/java/org/qora/test/btcacct/Refund2.java rename to src/test/java/org/qora/test/btcacct/Refund.java index 05801775..69cddde7 100644 --- a/src/test/java/org/qora/test/btcacct/Refund2.java +++ b/src/test/java/org/qora/test/btcacct/Refund.java @@ -3,16 +3,18 @@ package org.qora.test.btcacct; import java.security.Security; import java.time.Instant; import java.time.LocalDateTime; -import java.time.ZoneId; +import java.time.ZoneOffset; import java.util.List; import org.bitcoinj.core.Address; +import org.bitcoinj.core.AddressFormatException; 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.params.RegTestParams; import org.bitcoinj.params.TestNet3Params; import org.bitcoinj.script.Script.ScriptType; import org.bouncycastle.jce.provider.BouncyCastleProvider; @@ -46,7 +48,7 @@ import com.google.common.hash.HashCode; * */ -public class Refund2 { +public class Refund { static { // This must go before any calls to LogManager/Logger @@ -55,31 +57,57 @@ public class Refund2 { private static final long REFUND_TIMEOUT = 600L; // seconds - private static void usage() { - System.err.println(String.format("usage: Refund2 ")); - System.err.println(String.format("example: Refund2 027fb5828c5e201eaf6de4cd3b0b340d16a191ef848cd691f35ef8f727358c9c \\\n" - + "\t032783606be32a3e639a33afe2b15f058708ab124f3b290d595ee954390a0c8559 \\\n" - + "\tb837056cdc5d805e4db1f830a58158e1131ac96ea71de4c6f9d7854985e153e2 1575021641 2MvGdGUgAfc7qTHaZJwWmZ26Fg6Hjif8gNy")); + 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 03aa20871c2195361f2826c7a649eab6b42639630c4d8c33c55311d5c1e476b5d6 \\\n" + + "\tn2N5VKrzq39nmuefZwp3wBiF4icdXX2B6o \\\n" + + "\teb95e1c1a5e9e6733549faec85b71f74f67638ea63b0acf2f077e9d0cb94dfe8 1575653814 2Mtn4aLjjWVEWckdoTMK7P8WbkXJf1ES6yL")); System.exit(1); } public static void main(String[] args) { - if (args.length != 5) - usage(); + if (args.length < 5 || args.length > 6) + usage(null); Security.insertProviderAt(new BouncyCastleProvider(), 0); - Settings.fileInstance("settings-test.json"); + NetworkParameters params = RegTestParams.get(); + // TestNet3Params.get(); - NetworkParameters params = TestNet3Params.get(); + ECKey yourBitcoinKey = null; + Address theirBitcoinAddress = null; + byte[] tradePrivateKey = null; + int lockTime = 0; + Address p2shAddress = null; + Coin bitcoinFee = BTCACCT.DEFAULT_BTC_FEE; - int argIndex = 0; - String yourBitcoinPrivKeyHex = args[argIndex++]; - String theirBitcoinPubKeyHex = args[argIndex++]; + try { + int argIndex = 0; - String secretHashHex = args[argIndex++]; - String rawLockTime = args[argIndex++]; - String rawP2shAddress = args[argIndex++]; + yourBitcoinKey = ECKey.fromPublicOnly(HashCode.fromString(args[argIndex++]).asBytes()); + + theirBitcoinAddress = Address.fromString(params, args[argIndex++]); + if (theirBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH) + usage("Their BTC address is not in P2PKH form"); + + tradePrivateKey = HashCode.fromString(args[argIndex++]).asBytes(); + if (tradePrivateKey.length != 32) + usage("Trade private key not 32 bytes"); + + lockTime = Integer.parseInt(args[argIndex++]); + + p2shAddress = Address.fromString(params, args[argIndex++]); + if (p2shAddress.getOutputScriptType() != ScriptType.P2SH) + usage("P2SH address invalid"); + + if (args.length > argIndex) + bitcoinFee = Coin.parseCoin(args[argIndex++]); + } catch (NumberFormatException | AddressFormatException e) { + usage(String.format("Argument format exception: %s", e.getMessage())); + } try { RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl()); @@ -91,50 +119,47 @@ public class Refund2 { try (final Repository repository = RepositoryManager.getRepository()) { System.out.println("Confirm the following is correct based on the info you've given:"); - byte[] yourBitcoinPrivKey = HashCode.fromString(yourBitcoinPrivKeyHex).asBytes(); - ECKey yourBitcoinKey = ECKey.fromPrivate(yourBitcoinPrivKey); - Address yourBitcoinAddress = Address.fromKey(params, yourBitcoinKey, ScriptType.P2PKH); - System.out.println(String.format("Your Bitcoin address: %s", yourBitcoinAddress)); - - byte[] theirBitcoinPubKey = HashCode.fromString(theirBitcoinPubKeyHex).asBytes(); - ECKey theirBitcoinKey = ECKey.fromPublicOnly(theirBitcoinPubKey); - Address theirBitcoinAddress = Address.fromKey(params, theirBitcoinKey, ScriptType.P2PKH); + System.out.println(String.format("Your Bitcoin address: %s", Address.fromKey(params, yourBitcoinKey, ScriptType.P2PKH))); System.out.println(String.format("Their Bitcoin address: %s", theirBitcoinAddress)); + System.out.println(String.format("Trade PRIVATE key: %s", HashCode.fromBytes(tradePrivateKey))); + 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("Bitcoin redeem fee: %s", bitcoinFee.toPlainString())); // New/derived info - int lockTime = Integer.valueOf(rawLockTime); - System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneId.systemDefault()), lockTime)); + System.out.println("\nCHECKING info from other party:"); - byte[] secretHash = HashCode.fromString(secretHashHex).asBytes(); - System.out.println("Hash of secret: " + HashCode.fromBytes(secretHash).toString()); + ECKey tradeKey = ECKey.fromPrivate(tradePrivateKey); + System.out.println(String.format("Trade pubkeyhash: %s", HashCode.fromBytes(tradeKey.getPubKeyHash()))); - byte[] redeemScriptBytes = BTCACCT.buildRedeemScript(secretHash, yourBitcoinKey.getPubKey(), theirBitcoinPubKey, lockTime); - System.out.println("Redeem script: " + HashCode.fromBytes(redeemScriptBytes).toString()); + byte[] redeemScriptBytes = BTCACCT.buildScript(tradeKey.getPubKeyHash(), yourBitcoinKey.getPubKeyHash(), theirBitcoinAddress.getHash(), lockTime); + System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes))); byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes); + Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); - Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); - System.out.println(String.format("P2SH address: %s", p2shAddress)); - - if (!p2shAddress.toString().equals(rawP2shAddress)) { - System.err.println(String.format("Derived P2SH address %s does not match given address %s", p2shAddress, rawP2shAddress)); + if (!derivedP2shAddress.equals(p2shAddress)) { + System.err.println(String.format("Derived P2SH address %s does not match given address %s", derivedP2shAddress, p2shAddress)); System.exit(2); } // Some checks + + System.out.println("\nProcessing:"); + long medianBlockTime = BTC.getInstance().getMedianBlockTime(); - System.out.println(String.format("Median block time: %s", LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneId.systemDefault()))); + 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), ZoneId.systemDefault()), LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneId.systemDefault()))); + 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), ZoneId.systemDefault()), LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneId.systemDefault()))); + 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); } @@ -159,13 +184,17 @@ public class Refund2 { System.exit(2); } - Transaction refundTransaction = BTCACCT.buildRefundTransaction(p2shBalance, yourBitcoinKey, fundingOutputs.get(0), redeemScriptBytes, lockTime); + TransactionOutput fundingOutput = fundingOutputs.get(0); + System.out.println(String.format("Using output %s:%d for refund", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex())); + + Coin refundAmount = p2shBalance.subtract(bitcoinFee); + Transaction refundTransaction = BTCACCT.buildRefundTransaction(refundAmount, tradeKey, yourBitcoinKey.getPubKey(), fundingOutput, redeemScriptBytes, lockTime); byte[] refundBytes = refundTransaction.bitcoinSerialize(); - System.out.println(String.format("\nLoad this transaction into your wallet, sign and broadcast:\n%s\n", HashCode.fromBytes(refundBytes).toString())); + System.out.println(String.format("\nLoad this transaction into your wallet and broadcast:\n%s\n", HashCode.fromBytes(refundBytes).toString())); } catch (NumberFormatException e) { - usage(); + usage(String.format("Number format exception: %s", e.getMessage())); } catch (DataException e) { throw new RuntimeException("Repository issue: " + e.getMessage()); } diff --git a/src/test/java/org/qora/test/btcacct/Respond2.java b/src/test/java/org/qora/test/btcacct/Respond2.java index 9dac26ed..5699ad1a 100644 --- a/src/test/java/org/qora/test/btcacct/Respond2.java +++ b/src/test/java/org/qora/test/btcacct/Respond2.java @@ -61,7 +61,7 @@ public class Respond2 { private static final long REFUND_TIMEOUT = 600L; // seconds private static void usage() { - System.err.println(String.format("usage: Respond2 ")); + System.err.println(String.format("usage: Respond2 ")); System.err.println(String.format("example: Respond2 3jjoToDaDpsdUHqaouLGypFeewNVKvtkmdM38i54WVra \\\n" + "\t032783606be32a3e639a33afe2b15f058708ab124f3b290d595ee954390a0c8559 \\\n" + "\t123 0.00008642 \\\n" @@ -136,7 +136,7 @@ public class Respond2 { byte[] secretHash = HashCode.fromString(secretHashHex).asBytes(); System.out.println("Hash of secret: " + HashCode.fromBytes(secretHash).toString()); - byte[] redeemScriptBytes = BTCACCT.buildRedeemScript(secretHash, theirBitcoinPubKey, yourBitcoinPubKey, lockTime); + byte[] redeemScriptBytes = BTCACCT.buildScript(secretHash, theirBitcoinPubKey, yourBitcoinPubKey, lockTime); System.out.println("Redeem script: " + HashCode.fromBytes(redeemScriptBytes).toString()); byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes);