forked from Qortal/qortal
90094be95a
Some initial BTC cross-chain support. (Needs more work). Unified timestamp for V2 switchover to block version 4, applicable to several transaction types. Qora-specific interface to CIYAM ATv2 library. Beware: some areas still work-in-progress!
333 lines
14 KiB
Java
333 lines
14 KiB
Java
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) };
|
|
}
|
|
|
|
}
|