Qortal Core - Main Code Repository Decentralized Data Network - Blockchain - TRUE Cross-Chain Trading - Application and Website Hosting - Much More - Qortal is the future internet infrastructure for the global digital world. https://qortal.dev
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

332 lines
14 KiB

package test;
import java.io.File;
import java.net.UnknownHostException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.Security;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.InsufficientMoneyException;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.Transaction.SigHash;
import org.bitcoinj.core.TransactionBroadcast;
import org.bitcoinj.core.TransactionInput;
import org.bitcoinj.core.TransactionOutPoint;
import org.bitcoinj.crypto.TransactionSignature;
import org.bitcoinj.kits.WalletAppKit;
import org.bitcoinj.params.RegTestParams;
import org.bitcoinj.params.TestNet3Params;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptBuilder;
import org.bitcoinj.script.ScriptChunk;
import org.bitcoinj.script.ScriptOpCodes;
import org.bitcoinj.wallet.WalletTransaction.Pool;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.BeforeClass;
import org.junit.Test;
import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes;
/**
* 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 BTCACCTTests {
private static final long TIMEOUT = 600L;
private static final Coin sendValue = Coin.valueOf(6_000L);
private static final Coin fee = Coin.valueOf(2_000L);
private static final byte[] senderPrivKeyBytes = HashCode.fromString("027fb5828c5e201eaf6de4cd3b0b340d16a191ef848cd691f35ef8f727358c9c").asBytes();
private static final byte[] recipientPrivKeyBytes = HashCode.fromString("ec199a4abc9d3bf024349e397535dfee9d287e174aeabae94237eb03a0118c03").asBytes();
// The following need to be updated manually
private static final String prevTxHash = "70ee97f20afea916c2e7b47f6abf3c75f97c4c2251b4625419406a2dd47d16b5";
private static final Coin prevTxBalance = Coin.valueOf(562_000L); // This is NOT the amount but the unspent balance
private static final long prevTxOutputIndex = 1L;
// For when we want to re-run
private static final byte[] prevSecret = HashCode.fromString("30a13291e350214bea5318f990b77bc11d2cb709f7c39859f248bef396961dcc").asBytes();
private static final long prevLockTime = 1539347892L;
private static final boolean usePreviousFundingTx = true;
private static final boolean doRefundNotRedeem = false;
@BeforeClass
public static void beforeClass() {
Security.insertProviderAt(new BouncyCastleProvider(), 0);
}
@Test
public void buildBTCACCTTest() throws NoSuchAlgorithmException, InsufficientMoneyException, InterruptedException, ExecutionException, UnknownHostException {
byte[] secret = new byte[32];
new SecureRandom().nextBytes(secret);
if (usePreviousFundingTx)
secret = prevSecret;
System.out.println("Secret: " + HashCode.fromBytes(secret).toString());
MessageDigest sha256Digester = MessageDigest.getInstance("SHA-256");
byte[] secretHash = sha256Digester.digest(secret);
String secretHashHex = HashCode.fromBytes(secretHash).toString();
System.out.println("SHA256(secret): " + secretHashHex);
NetworkParameters params = TestNet3Params.get();
// NetworkParameters params = RegTestParams.get();
System.out.println("Network: " + params.getId());
WalletAppKit kit = new WalletAppKit(params, new File("."), "btc-tests");
kit.setBlockingStartup(false);
kit.startAsync();
kit.awaitRunning();
long now = System.currentTimeMillis() / 1000L;
long lockTime = now + TIMEOUT;
if (usePreviousFundingTx)
lockTime = prevLockTime;
System.out.println("LockTime: " + lockTime);
ECKey senderKey = ECKey.fromPrivate(senderPrivKeyBytes);
kit.wallet().importKey(senderKey);
ECKey recipientKey = ECKey.fromPrivate(recipientPrivKeyBytes);
kit.wallet().importKey(recipientKey);
byte[] senderPubKey = senderKey.getPubKey();
System.out.println("Sender address: " + senderKey.toAddress(params).toBase58());
System.out.println("Sender pubkey: " + HashCode.fromBytes(senderPubKey).toString());
byte[] recipientPubKey = recipientKey.getPubKey();
System.out.println("Recipient address: " + recipientKey.toAddress(params).toBase58());
System.out.println("Recipient pubkey: " + HashCode.fromBytes(recipientPubKey).toString());
byte[] redeemScriptBytes = buildRedeemScript(secret, senderPubKey, recipientPubKey, lockTime);
System.out.println("Redeem script: " + HashCode.fromBytes(redeemScriptBytes).toString());
byte[] redeemScriptHash = hash160(redeemScriptBytes);
Address p2shAddress = Address.fromP2SHHash(params, redeemScriptHash);
System.out.println("P2SH address: " + p2shAddress.toBase58());
// Send amount to P2SH address
Transaction fundingTransaction = buildFundingTransaction(params, Sha256Hash.wrap(prevTxHash), prevTxOutputIndex, prevTxBalance, senderKey,
sendValue.add(fee), redeemScriptHash);
System.out.println("Sending " + sendValue.add(fee).toPlainString() + " to " + p2shAddress.toBase58());
if (!usePreviousFundingTx)
broadcastWithConfirmation(kit, fundingTransaction);
if (doRefundNotRedeem) {
// Refund
System.out.println("Refunding " + sendValue.toPlainString() + " back to " + senderKey.toAddress(params));
now = System.currentTimeMillis() / 1000L;
long refundLockTime = now - 60 * 30; // 30 minutes in the past, needs to before 'now' and before "median block time" (median of previous 11 block
// timestamps)
if (refundLockTime < lockTime)
throw new RuntimeException("Too soon to refund");
TransactionOutPoint fundingOutPoint = new TransactionOutPoint(params, 0, fundingTransaction);
Transaction refundTransaction = buildRefundTransaction(params, fundingOutPoint, senderKey, sendValue, redeemScriptBytes, refundLockTime);
broadcastWithConfirmation(kit, refundTransaction);
} else {
// Redeem
System.out.println("Redeeming " + sendValue.toPlainString() + " to " + recipientKey.toAddress(params));
TransactionOutPoint fundingOutPoint = new TransactionOutPoint(params, 0, fundingTransaction);
Transaction redeemTransaction = buildRedeemTransaction(params, fundingOutPoint, recipientKey, sendValue, secret, redeemScriptBytes);
broadcastWithConfirmation(kit, redeemTransaction);
}
kit.wallet().cleanup();
for (Transaction transaction : kit.wallet().getTransactionPool(Pool.PENDING).values())
System.out.println("Pending tx: " + transaction.getHashAsString());
}
private static final byte[] redeemScript1 = HashCode.fromString("76a820").asBytes();
private static final byte[] redeemScript2 = HashCode.fromString("87637576a914").asBytes();
private static final byte[] redeemScript3 = HashCode.fromString("88ac6704").asBytes();
private static final byte[] redeemScript4 = HashCode.fromString("b17576a914").asBytes();
private static final byte[] redeemScript5 = HashCode.fromString("88ac68").asBytes();
private byte[] buildRedeemScript(byte[] secret, byte[] senderPubKey, byte[] recipientPubKey, long lockTime) {
try {
MessageDigest sha256Digester = MessageDigest.getInstance("SHA-256");
byte[] secretHash = sha256Digester.digest(secret);
byte[] senderPubKeyHash = hash160(senderPubKey);
byte[] recipientPubKeyHash = hash160(recipientPubKey);
return Bytes.concat(redeemScript1, secretHash, redeemScript2, recipientPubKeyHash, redeemScript3, toLEByteArray((int) (lockTime & 0xffffffffL)),
redeemScript4, senderPubKeyHash, redeemScript5);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Message digest unsupported", e);
}
}
private byte[] hash160(byte[] input) {
try {
MessageDigest rmd160Digester = MessageDigest.getInstance("RIPEMD160");
MessageDigest sha256Digester = MessageDigest.getInstance("SHA-256");
return rmd160Digester.digest(sha256Digester.digest(input));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Message digest unsupported", e);
}
}
private Transaction buildFundingTransaction(NetworkParameters params, Sha256Hash prevTxHash, long outputIndex, Coin balance, ECKey sigKey, Coin value,
byte[] redeemScriptHash) {
Transaction fundingTransaction = new Transaction(params);
// Outputs (needed before input so inputs can be signed)
// Fixed amount to P2SH
fundingTransaction.addOutput(value, ScriptBuilder.createP2SHOutputScript(redeemScriptHash));
// Change to sender
fundingTransaction.addOutput(balance.minus(value).minus(fee), ScriptBuilder.createOutputScript(sigKey.toAddress(params)));
// Input
// We create fake "to address" scriptPubKey for prev tx so our spending input is P2PKH type
Script fakeScriptPubKey = ScriptBuilder.createOutputScript(sigKey.toAddress(params));
TransactionOutPoint prevOut = new TransactionOutPoint(params, outputIndex, prevTxHash);
fundingTransaction.addSignedInput(prevOut, fakeScriptPubKey, sigKey);
return fundingTransaction;
}
private Transaction buildRedeemTransaction(NetworkParameters params, TransactionOutPoint fundingOutPoint, ECKey recipientKey, Coin value, byte[] secret,
byte[] redeemScriptBytes) {
Transaction redeemTransaction = new Transaction(params);
redeemTransaction.setVersion(2);
// Outputs
redeemTransaction.addOutput(value, ScriptBuilder.createOutputScript(recipientKey.toAddress(params)));
// Input
byte[] recipientPubKey = recipientKey.getPubKey();
ScriptBuilder scriptBuilder = new ScriptBuilder();
scriptBuilder.addChunk(new ScriptChunk(recipientPubKey.length, recipientPubKey));
scriptBuilder.addChunk(new ScriptChunk(secret.length, secret));
scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes));
byte[] scriptPubKey = scriptBuilder.build().getProgram();
TransactionInput input = new TransactionInput(params, null, scriptPubKey, fundingOutPoint);
input.setSequenceNumber(0xffffffffL); // Final
redeemTransaction.addInput(input);
// Generate transaction signature for input
boolean anyoneCanPay = false;
Sha256Hash hash = redeemTransaction.hashForSignature(0, redeemScriptBytes, SigHash.ALL, anyoneCanPay);
System.out.println("redeem transaction's input hash: " + hash.toString());
ECKey.ECDSASignature ecSig = recipientKey.sign(hash);
TransactionSignature txSig = new TransactionSignature(ecSig, SigHash.ALL, anyoneCanPay);
byte[] txSigBytes = txSig.encodeToBitcoin();
System.out.println("redeem transaction's signature: " + HashCode.fromBytes(txSigBytes).toString());
// Prepend signature to input
scriptBuilder.addChunk(0, new ScriptChunk(txSigBytes.length, txSigBytes));
input.setScriptSig(scriptBuilder.build());
return redeemTransaction;
}
private Transaction buildRefundTransaction(NetworkParameters params, TransactionOutPoint fundingOutPoint, ECKey senderKey, Coin value,
byte[] redeemScriptBytes, long lockTime) {
Transaction refundTransaction = new Transaction(params);
refundTransaction.setVersion(2);
// Outputs
refundTransaction.addOutput(value, ScriptBuilder.createOutputScript(senderKey.toAddress(params)));
// Input
byte[] recipientPubKey = senderKey.getPubKey();
ScriptBuilder scriptBuilder = new ScriptBuilder();
scriptBuilder.addChunk(new ScriptChunk(recipientPubKey.length, recipientPubKey));
scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes));
byte[] scriptPubKey = scriptBuilder.build().getProgram();
TransactionInput input = new TransactionInput(params, null, scriptPubKey, fundingOutPoint);
input.setSequenceNumber(0);
refundTransaction.addInput(input);
// Set locktime after input but before input signature is generated
refundTransaction.setLockTime(lockTime);
// Generate transaction signature for input
boolean anyoneCanPay = false;
Sha256Hash hash = refundTransaction.hashForSignature(0, redeemScriptBytes, SigHash.ALL, anyoneCanPay);
System.out.println("refund transaction's input hash: " + hash.toString());
ECKey.ECDSASignature ecSig = senderKey.sign(hash);
TransactionSignature txSig = new TransactionSignature(ecSig, SigHash.ALL, anyoneCanPay);
byte[] txSigBytes = txSig.encodeToBitcoin();
System.out.println("refund transaction's signature: " + HashCode.fromBytes(txSigBytes).toString());
// Prepend signature to input
scriptBuilder.addChunk(0, new ScriptChunk(txSigBytes.length, txSigBytes));
input.setScriptSig(scriptBuilder.build());
return refundTransaction;
}
private void broadcastWithConfirmation(WalletAppKit kit, Transaction transaction) {
System.out.println("Broadcasting tx: " + transaction.getHashAsString());
System.out.println("TX hex: " + HashCode.fromBytes(transaction.bitcoinSerialize()).toString());
System.out.println("Number of connected peers: " + kit.peerGroup().numConnectedPeers());
TransactionBroadcast txBroadcast = kit.peerGroup().broadcastTransaction(transaction);
try {
txBroadcast.future().get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException("Transaction broadcast failed", e);
}
// wait for confirmation
System.out.println("Waiting for confirmation of tx: " + transaction.getHashAsString());
try {
transaction.getConfidence().getDepthFuture(1).get();
} catch (CancellationException | ExecutionException | InterruptedException e) {
throw new RuntimeException("Transaction confirmation failed", e);
}
System.out.println("Confirmed tx: " + transaction.getHashAsString());
}
/** Convert int to little-endian byte array */
private byte[] toLEByteArray(int value) {
return new byte[] { (byte) (value), (byte) (value >> 8), (byte) (value >> 16), (byte) (value >> 24) };
}
}