forked from Qortal/qortal
Interim commit of BTC-QORT cross-chain trade, with partial conversion from secret+hash to using "trade key"
This commit is contained in:
parent
5c0134c16a
commit
2c4bad6455
@ -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.
|
||||
* <p>
|
||||
* <pre>
|
||||
* 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
|
||||
* </pre>
|
||||
*
|
||||
* @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;
|
||||
|
@ -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();
|
||||
|
69
src/test/java/org/qora/test/apps/BuildCheckpoints.java
Normal file
69
src/test/java/org/qora/test/apps/BuildCheckpoints.java
Normal file
@ -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<Integer, StoredBlock> 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
144
src/test/java/org/qora/test/btcacct/Initiate.java
Normal file
144
src/test/java/org/qora/test/btcacct/Initiate.java
Normal file
@ -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 <your-BTC-P2PKH> <BTC-amount> <their-BTC-P2PKH> (<BTC-redeem/refund-fee>)"));
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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 <your-QORT-PRIVkey> <your-BTC-pubkey> <QORT-amount> <BTC-amount> <their-QORT-pubkey> <their-BTC-pubkey>"));
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
199
src/test/java/org/qora/test/btcacct/Redeem.java
Normal file
199
src/test/java/org/qora/test/btcacct/Redeem.java
Normal file
@ -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 <your-BTC-pubkey> <their-BTC-P2PKH> <trade-PRIVATE-key> <locktime> <P2SH-address> (<BTC-redeem/refund-fee>)"));
|
||||
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<TransactionOutput> 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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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 <your-BTC-PRIVkey> <their-BTC-pubkey> <hash-of-secret> <locktime> <P2SH-address>"));
|
||||
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 <your-BTC-pubkey> <their-BTC-P2PKH> <trade-PRIVATE-key> <locktime> <P2SH-address> (<BTC-redeem/refund-fee>)"));
|
||||
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());
|
||||
}
|
@ -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 <your-QORT-PRIVkey> <your-BTC-pubkey> <QORT-amount> <BTC-amount> <their-QORT-pubkey> <their-BTC-pubkey> <hash-of-secret> <locktime> <P2SH-address>"));
|
||||
System.err.println(String.format("usage: Respond2 <your-BTC-pubkey> <BTC-amount> <their-BTC-pubkey> <trade-pubkeyhash> <locktime> <P2SH-address>"));
|
||||
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);
|
||||
|
Loading…
Reference in New Issue
Block a user