mirror of https://github.com/qortal/qortal
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
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) }; |
|
} |
|
|
|
}
|
|
|