diff --git a/lib/org/ciyam/at/1.2/at-1.2.jar b/lib/org/ciyam/at/1.2/at-1.2.jar index daa4c474..f343f85a 100644 Binary files a/lib/org/ciyam/at/1.2/at-1.2.jar and b/lib/org/ciyam/at/1.2/at-1.2.jar differ diff --git a/lib/org/ciyam/at/1.3/at-1.3.jar b/lib/org/ciyam/at/1.3/at-1.3.jar new file mode 100644 index 00000000..9393b73b Binary files /dev/null and b/lib/org/ciyam/at/1.3/at-1.3.jar differ diff --git a/lib/org/ciyam/at/1.3/at-1.3.pom b/lib/org/ciyam/at/1.3/at-1.3.pom new file mode 100644 index 00000000..c7091d47 --- /dev/null +++ b/lib/org/ciyam/at/1.3/at-1.3.pom @@ -0,0 +1,9 @@ + + + 4.0.0 + org.ciyam + at + 1.3 + POM was created from install:install-file + diff --git a/lib/org/ciyam/at/maven-metadata-local.xml b/lib/org/ciyam/at/maven-metadata-local.xml index 65fd3fa6..6e8cda0e 100644 --- a/lib/org/ciyam/at/maven-metadata-local.xml +++ b/lib/org/ciyam/at/maven-metadata-local.xml @@ -3,11 +3,12 @@ org.ciyam at - 1.2 + 1.3 1.0 1.2 + 1.3 - 20191121173210 + 20200408081355 diff --git a/pom.xml b/pom.xml index 067c359b..15509805 100644 --- a/pom.xml +++ b/pom.xml @@ -406,7 +406,7 @@ org.ciyam at - 1.2 + 1.3 diff --git a/src/main/java/org/qora/at/BlockchainAPI.java b/src/main/java/org/qora/at/BlockchainAPI.java deleted file mode 100644 index b8cd8c90..00000000 --- a/src/main/java/org/qora/at/BlockchainAPI.java +++ /dev/null @@ -1,153 +0,0 @@ -package org.qora.at; - -import static java.util.Arrays.stream; -import static java.util.stream.Collectors.toMap; - -import java.math.BigDecimal; -import java.util.List; -import java.util.Map; - -import org.ciyam.at.MachineState; -import org.ciyam.at.Timestamp; -import org.qora.account.Account; -import org.qora.block.Block; -import org.qora.data.block.BlockData; -import org.qora.data.transaction.ATTransactionData; -import org.qora.data.transaction.PaymentTransactionData; -import org.qora.data.transaction.TransactionData; -import org.qora.repository.BlockRepository; -import org.qora.repository.DataException; -import org.qora.transaction.Transaction; - -public enum BlockchainAPI { - - QORA(0) { - @Override - public void putTransactionFromRecipientAfterTimestampInA(String recipient, Timestamp timestamp, MachineState state) { - int height = timestamp.blockHeight; - int sequence = timestamp.transactionSequence + 1; - - QoraATAPI api = (QoraATAPI) state.getAPI(); - BlockRepository blockRepository = api.getRepository().getBlockRepository(); - - try { - Account recipientAccount = new Account(api.getRepository(), recipient); - - while (height <= blockRepository.getBlockchainHeight()) { - BlockData blockData = blockRepository.fromHeight(height); - - if (blockData == null) - throw new DataException("Unable to fetch block " + height + " from repository?"); - - Block block = new Block(api.getRepository(), blockData); - - List transactions = block.getTransactions(); - - // No more transactions in this block? Try next block - if (sequence >= transactions.size()) { - ++height; - sequence = 0; - continue; - } - - Transaction transaction = transactions.get(sequence); - - // Transaction needs to be sent to specified recipient - if (transaction.getRecipientAccounts().contains(recipientAccount)) { - // Found a transaction - - api.setA1(state, new Timestamp(height, timestamp.blockchainId, sequence).longValue()); - - // Hash transaction's signature into other three A fields for future verification that it's the same transaction - byte[] hash = QoraATAPI.sha192(transaction.getTransactionData().getSignature()); - - api.setA2(state, QoraATAPI.fromBytes(hash, 0)); - api.setA3(state, QoraATAPI.fromBytes(hash, 8)); - api.setA4(state, QoraATAPI.fromBytes(hash, 16)); - return; - } - - // Transaction wasn't for us - keep going - ++sequence; - } - - // No more transactions - zero A and exit - api.zeroA(state); - } catch (DataException e) { - throw new RuntimeException("AT API unable to fetch next transaction?", e); - } - } - - @Override - public long getAmountFromTransactionInA(Timestamp timestamp, MachineState state) { - QoraATAPI api = (QoraATAPI) state.getAPI(); - TransactionData transactionData = api.fetchTransaction(state); - - switch (transactionData.getType()) { - case PAYMENT: - return ((PaymentTransactionData) transactionData).getAmount().unscaledValue().longValue(); - - case AT: - BigDecimal amount = ((ATTransactionData) transactionData).getAmount(); - - if (amount != null) - return amount.unscaledValue().longValue(); - else - return 0xffffffffffffffffL; - - default: - return 0xffffffffffffffffL; - } - } - - @Override - public TransactionOutput getIndexedOutputFromTransactionInA(MachineState state, int outputIndex) { - // TODO - return null; - } - }, - BTC(1) { - @Override - public void putTransactionFromRecipientAfterTimestampInA(String recipient, Timestamp timestamp, MachineState state) { - // TODO BTC transaction support for ATv2 - } - - @Override - public long getAmountFromTransactionInA(Timestamp timestamp, MachineState state) { - // TODO BTC transaction support for ATv2 - return 0; - } - - @Override - public TransactionOutput getIndexedOutputFromTransactionInA(MachineState state, int outputIndex) { - // TODO - return null; - } - }; - - public static class TransactionOutput { - byte[] recipient; - long amount; - } - - public final int value; - - private static final Map map = stream(BlockchainAPI.values()).collect(toMap(type -> type.value, type -> type)); - - BlockchainAPI(int value) { - this.value = value; - } - - public static BlockchainAPI valueOf(int value) { - return map.get(value); - } - - // Blockchain-specific API methods - - public abstract void putTransactionFromRecipientAfterTimestampInA(String recipient, Timestamp timestamp, MachineState state); - - public abstract long getAmountFromTransactionInA(Timestamp timestamp, MachineState state); - - public abstract TransactionOutput getIndexedOutputFromTransactionInA(MachineState state, int outputIndex); - -} diff --git a/src/main/java/org/qora/crosschain/BTCACCT.java b/src/main/java/org/qora/crosschain/BTCACCT.java index 76a68ee6..2671d76f 100644 --- a/src/main/java/org/qora/crosschain/BTCACCT.java +++ b/src/main/java/org/qora/crosschain/BTCACCT.java @@ -1,7 +1,8 @@ package org.qora.crosschain; +import java.math.BigDecimal; import java.nio.ByteBuffer; -import java.nio.ByteOrder; +import java.util.function.Function; import org.bitcoinj.core.Coin; import org.bitcoinj.core.ECKey; @@ -11,6 +12,7 @@ import org.bitcoinj.core.Transaction.SigHash; import org.bitcoinj.core.TransactionInput; import org.bitcoinj.core.TransactionOutput; import org.bitcoinj.crypto.TransactionSignature; +import org.bitcoinj.script.Script; import org.bitcoinj.script.ScriptBuilder; import org.bitcoinj.script.ScriptChunk; import org.bitcoinj.script.ScriptOpCodes; @@ -26,124 +28,149 @@ public class BTCACCT { public static final Coin DEFAULT_BTC_FEE = Coin.valueOf(1000L); // 0.00001000 BTC - 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) + /* + * OP_TUCK (to copy public key to before signature) + * OP_CHECKSIGVERIFY (sig & pubkey must verify or script fails) + * OP_HASH160 (convert public key to PKH) + * OP_DUP (duplicate PKH) + * OP_EQUAL (does PKH match refund PKH?) + * OP_IF + * OP_DROP (no need for duplicate PKH) + * + * OP_CHECKLOCKTIMEVERIFY (if this passes, leftover stack is so script passes) + * OP_ELSE + * OP_EQUALVERIFY (duplicate PKH must match redeem PKH or script fails) + * OP_HASH160 (hash secret) + * OP_EQUAL (do hashes of secrets match? if true, script passes else script fails) + * OP_ENDIF + */ + + private static final byte[] redeemScript1 = HashCode.fromString("7dada97614").asBytes(); // OP_TUCK OP_CHECKSIGVERIFY OP_HASH160 OP_DUP push(0x14 bytes) + private static final byte[] redeemScript2 = HashCode.fromString("87637504").asBytes(); // OP_EQUAL OP_IF OP_DROP push(0x4 bytes) + private static final byte[] redeemScript3 = HashCode.fromString("b16714").asBytes(); // OP_CHECKLOCKTIMEVERIFY OP_ELSE push(0x14 bytes) + private static final byte[] redeemScript4 = HashCode.fromString("88a914").asBytes(); // OP_EQUALVERIFY OP_HASH160 push(0x14 bytes) private static final byte[] redeemScript5 = HashCode.fromString("8768").asBytes(); // OP_EQUAL OP_ENDIF /** * Returns Bitcoin redeem script. *

*

-	 * OP_DUP OP_HASH160 push(0x14) <trade pubkeyhash> OP_EQUALVERIFY OP_CHECKSIGVERIFY
-	 * OP_HASH160 OP_DUP push(0x14) <sender/refund P2PKH> OP_EQUAL
+	 * OP_TUCK OP_CHECKSIGVERIFY
+	 * OP_HASH160 OP_DUP push(0x14) <refunder pubkeyhash> OP_EQUAL
 	 * OP_IF
 	 * 	OP_DROP push(0x04 bytes) <refund locktime> OP_CHECKLOCKTIMEVERIFY
 	 * OP_ELSE
-	 *	push(0x14) <redeemer P2PKH> OP_EQUAL
+	 * 	push(0x14) <redeemer pubkeyhash> OP_EQUALVERIFY
+	 * 	OP_HASH160 push(0x14 bytes) <hash of secret> OP_EQUAL
 	 * OP_ENDIF
 	 * 
* - * @param tradePubKeyHash + * @param refunderPubKeyHash * @param senderPubKey * @param recipientPubKey * @param lockTime * @return */ - 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 byte[] buildScript(byte[] refunderPubKeyHash, int lockTime, byte[] redeemerPubKeyHash, byte[] secretHash) { + return Bytes.concat(redeemScript1, refunderPubKeyHash, redeemScript2, BitTwiddling.toLEByteArray((int) (lockTime & 0xffffffffL)), + redeemScript3, redeemerPubKeyHash, redeemScript4, secretHash, redeemScript5); } - public static Transaction buildRefundTransaction(Coin refundAmount, ECKey tradeKey, byte[] senderPubKey, TransactionOutput fundingOutput, byte[] redeemScriptBytes, long lockTime) { + /** + * Builds a custom transaction to spend P2SH. + * + * @param amount + * @param spendKey + * @param recipientPubKeyHash + * @param fundingOutput + * @param redeemScriptBytes + * @param lockTime + * @param scriptSigBuilder + * @return + */ + public static Transaction buildP2shTransaction(Coin amount, ECKey spendKey, TransactionOutput fundingOutput, byte[] redeemScriptBytes, Long lockTime, Function scriptSigBuilder) { NetworkParameters params = BTC.getInstance().getNetworkParameters(); - Transaction refundTransaction = new Transaction(params); - refundTransaction.setVersion(2); + Transaction transaction = new Transaction(params); + transaction.setVersion(2); // Output is back to P2SH funder - ECKey senderKey = ECKey.fromPublicOnly(senderPubKey); - refundTransaction.addOutput(refundAmount, ScriptBuilder.createP2PKHOutputScript(senderKey)); + transaction.addOutput(amount, ScriptBuilder.createP2PKHOutputScript(spendKey.getPubKeyHash())); // 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 - refundTransaction.addInput(input); + if (lockTime != null) + input.setSequenceNumber(BTC.LOCKTIME_NO_RBF_SEQUENCE); // Use max-value, so no lockTime and no RBF + else + input.setSequenceNumber(BTC.NO_LOCKTIME_NO_RBF_SEQUENCE); // Use max-value - 1, so lockTime can be used but not RBF + transaction.addInput(input); // Set locktime after inputs added but before input signatures are generated - refundTransaction.setLockTime(lockTime); + if (lockTime != null) + transaction.setLockTime(lockTime); // Generate transaction signature for input final boolean anyoneCanPay = false; - TransactionSignature txSig = refundTransaction.calculateSignature(0, tradeKey, redeemScriptBytes, SigHash.ALL, anyoneCanPay); + TransactionSignature txSig = transaction.calculateSignature(0, spendKey, redeemScriptBytes, SigHash.ALL, anyoneCanPay); - // Build scriptSig with... - ScriptBuilder scriptBuilder = new ScriptBuilder(); - - // sender/refund pubkey - scriptBuilder.addChunk(new ScriptChunk(senderPubKey.length, senderPubKey)); - - // transaction signature + // Calculate 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)); + // Build scriptSig using lambda and tx signature + Script scriptSig = scriptSigBuilder.apply(txSigBytes); // Set input scriptSig - refundTransaction.getInput(0).setScriptSig(scriptBuilder.build()); + transaction.getInput(0).setScriptSig(scriptSig); - return refundTransaction; + return transaction; } - public static Transaction buildRedeemTransaction(Coin redeemAmount, ECKey tradeKey, byte[] recipientPubKey, TransactionOutput fundingOutput, byte[] redeemScriptBytes) { - NetworkParameters params = BTC.getInstance().getNetworkParameters(); + public static Transaction buildRefundTransaction(Coin refundAmount, ECKey refundKey, TransactionOutput fundingOutput, byte[] redeemScriptBytes, long lockTime) { + Function refundSigScriptBuilder = (txSigBytes) -> { + // Build scriptSig with... + ScriptBuilder scriptBuilder = new ScriptBuilder(); - Transaction redeemTransaction = new Transaction(params); - redeemTransaction.setVersion(2); + // transaction signature + scriptBuilder.addChunk(new ScriptChunk(txSigBytes.length, txSigBytes)); - // Output to redeem recipient - ECKey senderKey = ECKey.fromPublicOnly(recipientPubKey); - redeemTransaction.addOutput(redeemAmount, ScriptBuilder.createP2PKHOutputScript(senderKey)); + // redeem public key + byte[] refundPubKey = refundKey.getPubKey(); + scriptBuilder.addChunk(new ScriptChunk(refundPubKey.length, refundPubKey)); - // 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); + // redeem script + scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes)); - // Generate transaction signature for input - final boolean anyoneCanPay = false; - TransactionSignature txSig = redeemTransaction.calculateSignature(0, tradeKey, redeemScriptBytes, SigHash.ALL, anyoneCanPay); + return scriptBuilder.build(); + }; - // 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; + return buildP2shTransaction(refundAmount, refundKey, fundingOutput, redeemScriptBytes, lockTime, refundSigScriptBuilder); } - public static byte[] buildCiyamAT(byte[] secretHash, byte[] destinationQortalPubKey, long refundMinutes) { + public static Transaction buildRedeemTransaction(Coin redeemAmount, ECKey redeemKey, TransactionOutput fundingOutput, byte[] redeemScriptBytes, byte[] secret) { + Function redeemSigScriptBuilder = (txSigBytes) -> { + // Build scriptSig with... + ScriptBuilder scriptBuilder = new ScriptBuilder(); + + // secret + scriptBuilder.addChunk(new ScriptChunk(secret.length, secret)); + + // transaction signature + scriptBuilder.addChunk(new ScriptChunk(txSigBytes.length, txSigBytes)); + + // redeem public key + byte[] redeemPubKey = redeemKey.getPubKey(); + scriptBuilder.addChunk(new ScriptChunk(redeemPubKey.length, redeemPubKey)); + + // redeem script + scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes)); + + return scriptBuilder.build(); + }; + + return buildP2shTransaction(redeemAmount, redeemKey, fundingOutput, redeemScriptBytes, null, redeemSigScriptBuilder); + } + + public static byte[] buildQortalAT(byte[] secretHash, String destinationQortalAddress, long refundMinutes, BigDecimal initialPayout) { // Labels for data segment addresses int addrCounter = 0; final int addrHashPart1 = addrCounter++; @@ -155,6 +182,9 @@ public class BTCACCT { final int addrAddressPart3 = addrCounter++; final int addrAddressPart4 = addrCounter++; final int addrRefundMinutes = addrCounter++; + final int addrHashTempIndex = addrCounter++; + final int addrHashTempLength = addrCounter++; + final int addrInitialPayoutAmount = addrCounter++; final int addrRefundTimestamp = addrCounter++; final int addrLastTimestamp = addrCounter++; final int addrBlockTimestamp = addrCounter++; @@ -164,19 +194,30 @@ public class BTCACCT { final int addrAddressTemp2 = addrCounter++; final int addrAddressTemp3 = addrCounter++; final int addrAddressTemp4 = addrCounter++; + final int addrHashTemp1 = addrCounter++; + final int addrHashTemp2 = addrCounter++; + final int addrHashTemp3 = addrCounter++; + final int addrHashTemp4 = addrCounter++; // Data segment - ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * 8).order(ByteOrder.LITTLE_ENDIAN); + ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * 8); // Hash of secret into HashPart1-4 dataByteBuffer.put(secretHash); // Destination Qortal account's public key - dataByteBuffer.put(destinationQortalPubKey); + dataByteBuffer.put(Bytes.ensureCapacity(destinationQortalAddress.getBytes(), 32, 0)); // Expiry in minutes dataByteBuffer.putLong(refundMinutes); + // Temp buffer for hashing any passed secret + dataByteBuffer.putLong(addrHashTemp1); + dataByteBuffer.putLong(32L); + + // Initial payout amount + dataByteBuffer.putLong(initialPayout.unscaledValue().longValue()); + // Code labels final int addrTxLoop = 0x36; final int addrCheckTx = 0x4b; @@ -187,13 +228,17 @@ public class BTCACCT { final int addrEndOfCode = 0x109; int tempPC; - ByteBuffer codeByteBuffer = ByteBuffer.allocate(addrEndOfCode * 1).order(ByteOrder.LITTLE_ENDIAN); + ByteBuffer codeByteBuffer = ByteBuffer.allocate(addrEndOfCode * 1); // init: codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_CREATION_TIMESTAMP.value).putInt(addrRefundTimestamp); codeByteBuffer.put(OpCode.SET_DAT.value).putInt(addrLastTimestamp).putInt(addrRefundTimestamp); codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.value).putShort(FunctionCode.ADD_MINUTES_TO_TIMESTAMP.value).putInt(addrRefundTimestamp) .putInt(addrRefundTimestamp).putInt(addrRefundMinutes); + + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B_IND.value).putInt(addrAddressPart1); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.PAY_TO_ADDRESS_IN_B.value).putInt(addrInitialPayoutAmount); + codeByteBuffer.put(OpCode.SET_PCS.value); // loop: @@ -204,7 +249,7 @@ public class BTCACCT { // txloop: assert codeByteBuffer.position() == addrTxLoop : "addrTxLoop incorrect"; - codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.PUT_TX_AFTER_TIMESTAMP_IN_A.value).putInt(addrLastTimestamp); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A.value).putInt(addrLastTimestamp); codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.CHECK_A_IS_ZERO.value).putInt(addrComparator); tempPC = codeByteBuffer.position(); codeByteBuffer.put(OpCode.BZR_DAT.value).putInt(addrComparator).put((byte) (addrCheckTx - tempPC)); @@ -220,10 +265,7 @@ public class BTCACCT { // checkSender assert codeByteBuffer.position() == addrCheckSender : "addrCheckSender incorrect"; codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.PUT_ADDRESS_FROM_TX_IN_A_INTO_B.value); - codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_B1.value).putInt(addrAddressTemp1); - codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_B2.value).putInt(addrAddressTemp2); - codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_B3.value).putInt(addrAddressTemp3); - codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_B4.value).putInt(addrAddressTemp4); + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_B_IND.value).putInt(addrAddressTemp1); tempPC = codeByteBuffer.position(); codeByteBuffer.put(OpCode.BNE_DAT.value).putInt(addrAddressTemp1).putInt(addrAddressPart1).put((byte) (addrTxLoop - tempPC)); tempPC = codeByteBuffer.position(); @@ -236,23 +278,16 @@ public class BTCACCT { // checkMessage: assert codeByteBuffer.position() == addrCheckMessage : "addrCheckMessage incorrect"; codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B.value); - codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.SWAP_A_AND_B.value); - codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B1.value).putInt(addrHashPart1); - codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B2.value).putInt(addrHashPart2); - codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B3.value).putInt(addrHashPart3); - codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B4.value).putInt(addrHashPart4); - codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.CHECK_SHA256_A_WITH_B.value).putInt(addrComparator); + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.GET_B_IND.value).putInt(addrHashTemp1); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B_IND.value).putInt(addrHashPart1); + codeByteBuffer.put(OpCode.EXT_FUN_RET.value).putShort(FunctionCode.CHECK_HASH160_WITH_B.value).putInt(addrHashTempIndex).putInt(addrHashTempLength); tempPC = codeByteBuffer.position(); codeByteBuffer.put(OpCode.BNZ_DAT.value).putInt(addrComparator).put((byte) (addrPayout - tempPC)); codeByteBuffer.put(OpCode.JMP_ADR.value).putInt(addrTxLoop); // payout: assert codeByteBuffer.position() == addrPayout : "addrPayout incorrect"; - codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B1.value).putInt(addrAddressPart1); - codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B2.value).putInt(addrAddressPart2); - codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B3.value).putInt(addrAddressPart3); - codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B4.value).putInt(addrAddressPart4); - codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.MESSAGE_A_TO_ADDRESS_IN_B.value); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.value).putShort(FunctionCode.SET_B_IND.value).putInt(addrAddressPart1); codeByteBuffer.put(OpCode.EXT_FUN.value).putShort(FunctionCode.PAY_ALL_TO_ADDRESS_IN_B.value); codeByteBuffer.put(OpCode.FIN_IMD.value); diff --git a/src/main/java/org/qortal/at/QortalATAPI.java b/src/main/java/org/qortal/at/QortalATAPI.java index 9be0e4c5..9016bb17 100644 --- a/src/main/java/org/qortal/at/QortalATAPI.java +++ b/src/main/java/org/qortal/at/QortalATAPI.java @@ -17,19 +17,24 @@ import org.qortal.account.Account; import org.qortal.account.GenesisAccount; import org.qortal.account.PublicKeyAccount; import org.qortal.asset.Asset; +import org.qortal.block.Block; import org.qortal.block.BlockChain; import org.qortal.block.BlockChain.CiyamAtSettings; import org.qortal.crypto.Crypto; import org.qortal.data.at.ATData; import org.qortal.data.block.BlockData; +import org.qortal.data.block.BlockSummaryData; import org.qortal.data.transaction.ATTransactionData; import org.qortal.data.transaction.BaseTransactionData; import org.qortal.data.transaction.MessageTransactionData; +import org.qortal.data.transaction.PaymentTransactionData; import org.qortal.data.transaction.TransactionData; import org.qortal.group.Group; +import org.qortal.repository.BlockRepository; import org.qortal.repository.DataException; import org.qortal.repository.Repository; import org.qortal.transaction.AtTransaction; +import org.qortal.transaction.Transaction; import com.google.common.primitives.Bytes; @@ -108,31 +113,90 @@ public class QortalATAPI extends API { } @Override - public void putPreviousBlockHashInA(MachineState state) { + public void putPreviousBlockHashIntoA(MachineState state) { try { - BlockData blockData = this.repository.getBlockRepository().fromHeight(this.getPreviousBlockHeight()); + int previousBlockHeight = this.repository.getBlockRepository().getBlockchainHeight() - 1; + + // We only need signature, so only request a block summary + List blockSummaries = this.repository.getBlockRepository().getBlockSummaries(previousBlockHeight, previousBlockHeight); + if (blockSummaries == null || blockSummaries.size() != 1) + throw new RuntimeException("AT API unable to fetch previous block hash?"); // Block's signature is 128 bytes so we need to reduce this to 4 longs (32 bytes) - byte[] blockHash = Crypto.digest(blockData.getSignature()); + // To be able to use hash to look up block, save height (8 bytes) and rehash with SHA192 (24 bytes) + this.setA1(state, previousBlockHeight); - this.setA(state, blockHash); + byte[] sigHash192 = sha192(blockSummaries.get(0).getSignature()); + this.setA2(state, fromBytes(sigHash192, 0)); + this.setA3(state, fromBytes(sigHash192, 8)); + this.setA4(state, fromBytes(sigHash192, 16)); } catch (DataException e) { throw new RuntimeException("AT API unable to fetch previous block?", e); } } @Override - public void putTransactionAfterTimestampInA(Timestamp timestamp, MachineState state) { + public void putTransactionAfterTimestampIntoA(Timestamp timestamp, MachineState state) { // Recipient is this AT String recipient = this.atData.getATAddress(); - BlockchainAPI blockchainAPI = BlockchainAPI.valueOf(timestamp.blockchainId); - blockchainAPI.putTransactionFromRecipientAfterTimestampInA(recipient, timestamp, state); + int height = timestamp.blockHeight; + int sequence = timestamp.transactionSequence + 1; + + BlockRepository blockRepository = this.getRepository().getBlockRepository(); + + try { + Account recipientAccount = new Account(this.getRepository(), recipient); + int currentHeight = blockRepository.getBlockchainHeight(); + + while (height <= currentHeight) { + BlockData blockData = blockRepository.fromHeight(height); + + if (blockData == null) + throw new DataException("Unable to fetch block " + height + " from repository?"); + + Block block = new Block(this.getRepository(), blockData); + + List blockTransactions = block.getTransactions(); + + // No more transactions in this block? Try next block + if (sequence >= blockTransactions.size()) { + ++height; + sequence = 0; + continue; + } + + Transaction transaction = blockTransactions.get(sequence); + + // Transaction needs to be sent to specified recipient + if (transaction.getRecipientAccounts().contains(recipientAccount)) { + // Found a transaction + + this.setA1(state, new Timestamp(height, timestamp.blockchainId, sequence).longValue()); + + // Hash transaction's signature into other three A fields for future verification that it's the same transaction + byte[] sigHash192 = sha192(transaction.getTransactionData().getSignature()); + this.setA2(state, fromBytes(sigHash192, 0)); + this.setA3(state, fromBytes(sigHash192, 8)); + this.setA4(state, fromBytes(sigHash192, 16)); + + return; + } + + // Transaction wasn't for us - keep going + ++sequence; + } + + // No more transactions - zero A and exit + this.zeroA(state); + } catch (DataException e) { + throw new RuntimeException("AT API unable to fetch next transaction?", e); + } } @Override public long getTypeFromTransactionInA(MachineState state) { - TransactionData transactionData = this.fetchTransaction(state); + TransactionData transactionData = this.getTransactionFromA(state); switch (transactionData.getType()) { case PAYMENT: @@ -154,9 +218,23 @@ public class QortalATAPI extends API { @Override public long getAmountFromTransactionInA(MachineState state) { - Timestamp timestamp = new Timestamp(state.getA1()); - BlockchainAPI blockchainAPI = BlockchainAPI.valueOf(timestamp.blockchainId); - return blockchainAPI.getAmountFromTransactionInA(timestamp, state); + TransactionData transactionData = this.getTransactionFromA(state); + + switch (transactionData.getType()) { + case PAYMENT: + return ((PaymentTransactionData) transactionData).getAmount().unscaledValue().longValue(); + + case AT: + BigDecimal amount = ((ATTransactionData) transactionData).getAmount(); + + if (amount != null) + return amount.unscaledValue().longValue(); + + // fall-through to default + + default: + return 0xffffffffffffffffL; + } } @Override @@ -168,8 +246,8 @@ public class QortalATAPI extends API { @Override public long generateRandomUsingTransactionInA(MachineState state) { - // The plan here is to sleep for a block then use next block's signature and this transaction's signature to generate pseudo-random, but deterministic, - // value. + // The plan here is to sleep for a block then use next block's signature + // and this transaction's signature to generate pseudo-random, but deterministic, value. if (!isFirstOpCodeAfterSleeping(state)) { // First call @@ -182,7 +260,7 @@ public class QortalATAPI extends API { // Second call // HASH(A and new block hash) - TransactionData transactionData = this.fetchTransaction(state); + TransactionData transactionData = this.getTransactionFromA(state); try { BlockData blockData = this.repository.getBlockRepository().getLastBlock(); @@ -206,7 +284,7 @@ public class QortalATAPI extends API { // Zero B in case of issues or shorter-than-B message this.zeroB(state); - TransactionData transactionData = this.fetchTransaction(state); + TransactionData transactionData = this.getTransactionFromA(state); byte[] messageData = null; @@ -236,7 +314,7 @@ public class QortalATAPI extends API { @Override public void putAddressFromTransactionInAIntoB(MachineState state) { - TransactionData transactionData = this.fetchTransaction(state); + TransactionData transactionData = this.getTransactionFromA(state); // We actually use public key as it has more potential utility (e.g. message verification) than an address byte[] bytes = transactionData.getCreatorPublicKey(); @@ -265,9 +343,7 @@ public class QortalATAPI extends API { @Override public void payAmountToB(long unscaledAmount, MachineState state) { - byte[] publicKey = state.getB(); - - PublicKeyAccount recipient = new PublicKeyAccount(this.repository, publicKey); + Account recipient = getAccountFromB(state); long timestamp = this.getNextTransactionTimestamp(); byte[] reference = this.getLastReference(); @@ -285,9 +361,7 @@ public class QortalATAPI extends API { @Override public void messageAToB(MachineState state) { byte[] message = state.getA(); - byte[] publicKey = state.getB(); - - PublicKeyAccount recipient = new PublicKeyAccount(this.repository, publicKey); + Account recipient = getAccountFromB(state); long timestamp = this.getNextTransactionTimestamp(); byte[] reference = this.getLastReference(); @@ -306,7 +380,7 @@ public class QortalATAPI extends API { int blockHeight = timestamp.blockHeight; // At least one block in the future - blockHeight += (minutes / this.ciyamAtSettings.minutesPerBlock) + 1; + blockHeight += Math.max(minutes / this.ciyamAtSettings.minutesPerBlock, 1); return new Timestamp(blockHeight, 0).longValue(); } @@ -380,7 +454,7 @@ public class QortalATAPI extends API { } /** Returns transaction data from repository using block height & sequence from A1, checking the transaction signatures match too */ - /* package */ TransactionData fetchTransaction(MachineState state) { + /* package */ TransactionData getTransactionFromA(MachineState state) { Timestamp timestamp = new Timestamp(state.getA1()); try { @@ -415,11 +489,6 @@ public class QortalATAPI extends API { * Timestamp is block's timestamp + position in AT-Transactions list. * * We need increasing timestamps to preserve transaction order and hence a correct signature-reference chain when the block is processed. - * - * As Qora blocks must share the same milliseconds component in their timestamps, this allows us to generate up to 1,000 AT-Transactions per AT without - * issue. - * - * As long as ATs are not allowed to generate that many per block, e.g. by limiting maximum steps per execution round, then we should be fine. */ // XXX THE ABOVE IS NO LONGER TRUE IN QORTAL! @@ -443,4 +512,27 @@ public class QortalATAPI extends API { } } + /** + * Returns Account (possibly PublicKeyAccount) based on value in B. + *

+ * If bytes in B start with 'Q' then use B as an address, but only if valid. + *

+ * Otherwise, assume B is a public key. + * @return + */ + private Account getAccountFromB(MachineState state) { + byte[] bBytes = state.getB(); + + if (bBytes[0] == 'Q') { + int zeroIndex = Bytes.indexOf(bBytes, (byte) 0); + if (zeroIndex > 0) { + String address = new String(bBytes, 0, zeroIndex); + if (Crypto.isValidAddress(address)) + return new Account(this.repository, address); + } + } + + return new PublicKeyAccount(this.repository, bBytes); + } + } diff --git a/src/main/java/org/qortal/at/QortalFunctionCode.java b/src/main/java/org/qortal/at/QortalFunctionCode.java index 1c68b244..e0d6cfe2 100644 --- a/src/main/java/org/qortal/at/QortalFunctionCode.java +++ b/src/main/java/org/qortal/at/QortalFunctionCode.java @@ -1,6 +1,5 @@ package org.qortal.at; -import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Map; import java.util.stream.Collectors; @@ -10,6 +9,7 @@ import org.ciyam.at.FunctionData; import org.ciyam.at.IllegalFunctionCodeException; import org.ciyam.at.MachineState; import org.ciyam.at.Timestamp; +import org.qortal.crosschain.BTC; import org.qortal.crypto.Crypto; import org.qortal.settings.Settings; @@ -20,52 +20,6 @@ import org.qortal.settings.Settings; * */ public enum QortalFunctionCode { - /** - * 0x0500
- * Returns current BTC block's "timestamp". - */ - GET_BTC_BLOCK_TIMESTAMP(0x0500, 0, true) { - @Override - protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { - functionData.returnValue = Timestamp.toLong(state.getAPI().getCurrentBlockHeight(), BlockchainAPI.BTC.value, 0); - } - }, - /** - * 0x0501
- * Put transaction from specific recipient after timestamp in A, or zero if none. - */ - PUT_TX_FROM_B_RECIPIENT_AFTER_TIMESTAMP_IN_A(0x0501, 1, false) { - @Override - protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { - Timestamp timestamp = new Timestamp(functionData.value2); - - String recipient = new String(state.getB(), StandardCharsets.UTF_8); - - BlockchainAPI blockchainAPI = BlockchainAPI.valueOf(timestamp.blockchainId); - blockchainAPI.putTransactionFromRecipientAfterTimestampInA(recipient, timestamp, state); - } - }, - /** - * 0x0502
- * Get output, using transaction in A and passed index, putting address in B and returning amount.
- * Return -1 if no such output; - */ - GET_INDEXED_OUTPUT(0x0502, 1, true) { - @Override - protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { - int outputIndex = (int) (functionData.value1 & 0xffffffffL); - - BlockchainAPI.TransactionOutput output = BlockchainAPI.BTC.getIndexedOutputFromTransactionInA(state, outputIndex); - - if (output == null) { - functionData.returnValue = -1L; - return; - } - - state.getAPI().setB(state, output.recipient); - functionData.returnValue = output.amount; - } - }, /** * 0x0510
* Convert address in B to 20-byte value in LSB of B1, and all of B2 & B3. @@ -90,7 +44,7 @@ public enum QortalFunctionCode { CONVERT_B_TO_P2SH(0x0511, 0, false) { @Override protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException { - byte addressPrefix = Settings.getInstance().useBitcoinTestNet() ? (byte) 0xc4 : 0x05; + byte addressPrefix = Settings.getInstance().getBitcoinNet() == BTC.BitcoinNet.MAIN ? 0x05 : (byte) 0xc4; convertAddressInB(addressPrefix, state); } diff --git a/src/main/java/org/qortal/crosschain/BTC.java b/src/main/java/org/qortal/crosschain/BTC.java index f46e0536..43001c6a 100644 --- a/src/main/java/org/qortal/crosschain/BTC.java +++ b/src/main/java/org/qortal/crosschain/BTC.java @@ -10,8 +10,11 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; +import java.net.InetAddress; +import java.net.UnknownHostException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.security.DigestOutputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -19,8 +22,9 @@ import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneOffset; import java.util.ArrayList; +import java.util.Collection; import java.util.List; -import java.util.function.Function; +import java.util.TreeMap; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -46,16 +50,19 @@ import org.bitcoinj.script.Script.ScriptType; import org.bitcoinj.store.BlockStore; import org.bitcoinj.store.BlockStoreException; import org.bitcoinj.store.MemoryBlockStore; +import org.bitcoinj.utils.MonetaryFormat; import org.bitcoinj.utils.Threading; import org.bitcoinj.wallet.Wallet; -import org.bitcoinj.wallet.WalletTransaction; -import org.bitcoinj.wallet.WalletTransaction.Pool; import org.bitcoinj.wallet.listeners.WalletCoinsReceivedEventListener; import org.bitcoinj.wallet.listeners.WalletCoinsSentEventListener; import org.qortal.settings.Settings; public class BTC { + public static final MonetaryFormat FORMAT = new MonetaryFormat().minDecimals(8).postfixCode(); + public static final long NO_LOCKTIME_NO_RBF_SEQUENCE = 0xFFFFFFFFL; + public static final long LOCKTIME_NO_RBF_SEQUENCE = NO_LOCKTIME_NO_RBF_SEQUENCE - 1; + private static final MessageDigest RIPE_MD160_DIGESTER; private static final MessageDigest SHA256_DIGESTER; static { @@ -69,6 +76,29 @@ public class BTC { protected static final Logger LOGGER = LogManager.getLogger(BTC.class); + public enum BitcoinNet { + MAIN { + @Override + public NetworkParameters getParams() { + return MainNetParams.get(); + } + }, + TEST3 { + @Override + public NetworkParameters getParams() { + return TestNet3Params.get(); + } + }, + REGTEST { + @Override + public NetworkParameters getParams() { + return RegTestParams.get(); + } + }; + + public abstract NetworkParameters getParams(); + } + private static BTC instance; private final NetworkParameters params; @@ -85,24 +115,60 @@ public class BTC { private static final String MINIMAL_TESTNET3_TEXTFILE = "TXT CHECKPOINTS 1\n0\n1\nAAAAAAAAB+EH4QfhAAAH4AEAAAApmwX6UCEnJcYIKTa7HO3pFkqqNhAzJVBMdEuGAAAAAPSAvVCBUypCbBW/OqU0oIF7ISF84h2spOqHrFCWN9Zw6r6/T///AB0E5oOO\n"; private static final String MINIMAL_MAINNET_TEXTFILE = "TXT CHECKPOINTS 1\n0\n1\nAAAAAAAAB+EH4QfhAAAH4AEAAABjl7tqvU/FIcDT9gcbVlA4nwtFUbxAtOawZzBpAAAAAKzkcK7NqciBjI/ldojNKncrWleVSgDfBCCn3VRrbSxXaw5/Sf//AB0z8Bkv\n"; - public UpdateableCheckpointManager(NetworkParameters params) throws IOException { - super(params, getMinimalTextFileStream(params)); + public UpdateableCheckpointManager(NetworkParameters params, File checkpointsFile) throws IOException { + super(params, getMinimalTextFileStream(params, checkpointsFile)); } public UpdateableCheckpointManager(NetworkParameters params, InputStream inputStream) throws IOException { super(params, inputStream); } - private static ByteArrayInputStream getMinimalTextFileStream(NetworkParameters params) { + private static ByteArrayInputStream getMinimalTextFileStream(NetworkParameters params, File checkpointsFile) { if (params == MainNetParams.get()) return new ByteArrayInputStream(MINIMAL_MAINNET_TEXTFILE.getBytes()); if (params == TestNet3Params.get()) return new ByteArrayInputStream(MINIMAL_TESTNET3_TEXTFILE.getBytes()); + if (params == RegTestParams.get()) + return newRegTestCheckpointsStream(checkpointsFile); // We have to build this + throw new RuntimeException("Failed to construct empty UpdateableCheckpointManageer"); } + private static ByteArrayInputStream newRegTestCheckpointsStream(File checkpointsFile) { + try { + 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.getLoopbackAddress(); + final PeerAddress peerAddress = new PeerAddress(params, ipAddress); + peerGroup.addAddress(peerAddress); + peerGroup.start(); + + final TreeMap checkpoints = new TreeMap<>(); + chain.addNewBestBlockListener((block) -> checkpoints.put(block.getHeight(), block)); + + peerGroup.downloadBlockChain(); + peerGroup.stop(); + + saveAsText(checkpointsFile, checkpoints.values()); + + return new ByteArrayInputStream(Files.readAllBytes(checkpointsFile.toPath())); + } catch (BlockStoreException e) { + throw new RuntimeException(e); + } catch (UnknownHostException e) { + throw new RuntimeException(e); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + @Override public void notifyNewBestBlock(StoredBlock block) { final int height = block.getHeight(); @@ -119,22 +185,22 @@ public class BTC { this.checkpoints.put(blockTimestamp, block); try { - this.saveAsText(new File(BTC.getInstance().getDirectory(), BTC.getInstance().getCheckpointsFileName())); + saveAsText(new File(BTC.getInstance().getDirectory(), BTC.getInstance().getCheckpointsFileName()), this.checkpoints.values()); } catch (FileNotFoundException e) { // Save failed - log it but it's not critical LOGGER.warn("Failed to save updated BTC checkpoints: " + e.getMessage()); } } - public void saveAsText(File textFile) throws FileNotFoundException { + private static void saveAsText(File textFile, Collection checkpointBlocks) throws FileNotFoundException { 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(this.checkpoints.size()); + writer.println(checkpointBlocks.size()); ByteBuffer buffer = ByteBuffer.allocate(StoredBlock.COMPACT_SERIALIZED_SIZE); - for (StoredBlock block : this.checkpoints.values()) { + for (StoredBlock block : checkpointBlocks) { block.serializeCompact(buffer); writer.println(CheckpointManager.BASE64.encode(buffer.array())); buffer.position(0); @@ -173,16 +239,24 @@ public class BTC { // Constructors and instance private BTC() { - if (Settings.getInstance().useBitcoinTestNet()) { - /* - this.params = RegTestParams.get(); - this.checkpointsFileName = "checkpoints-regtest.txt"; - */ - this.params = TestNet3Params.get(); - this.checkpointsFileName = "checkpoints-testnet.txt"; - } else { - this.params = MainNetParams.get(); - this.checkpointsFileName = "checkpoints.txt"; + BitcoinNet bitcoinNet = Settings.getInstance().getBitcoinNet(); + this.params = bitcoinNet.getParams(); + + switch (bitcoinNet) { + case MAIN: + this.checkpointsFileName = "checkpoints.txt"; + break; + + case TEST3: + this.checkpointsFileName = "checkpoints-testnet.txt"; + break; + + case REGTEST: + this.checkpointsFileName = "checkpoints-regtest.txt"; + break; + + default: + throw new IllegalStateException("Unsupported Bitcoin network: " + bitcoinNet.name()); } this.directory = new File("Qortal-BTC"); @@ -196,7 +270,7 @@ public class BTC { } catch (FileNotFoundException e) { // Construct with no checkpoints then try { - this.manager = new UpdateableCheckpointManager(this.params); + this.manager = new UpdateableCheckpointManager(this.params, checkpointsFile); } catch (IOException e2) { throw new RuntimeException("Failed to create new BTC checkpoints", e2); } @@ -222,7 +296,7 @@ public class BTC { return this.checkpointsFileName; } - /* package */ NetworkParameters getNetworkParameters() { + public NetworkParameters getNetworkParameters() { return this.params; } diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java index 83d46d8b..7ed761cd 100644 --- a/src/main/java/org/qortal/settings/Settings.java +++ b/src/main/java/org/qortal/settings/Settings.java @@ -20,6 +20,7 @@ import org.eclipse.persistence.exceptions.XMLMarshalException; import org.eclipse.persistence.jaxb.JAXBContextFactory; import org.eclipse.persistence.jaxb.UnmarshallerProperties; import org.qortal.block.BlockChain; +import org.qortal.crosschain.BTC.BitcoinNet; // All properties to be converted to JSON via JAXB @XmlAccessorType(XmlAccessType.FIELD) @@ -91,7 +92,7 @@ public class Settings { // Which blockchains this node is running private String blockchainConfig = null; // use default from resources - private boolean useBitcoinTestNet = false; + private BitcoinNet bitcoinNet = BitcoinNet.MAIN; // Repository related /** Queries that take longer than this are logged. (milliseconds) */ @@ -345,8 +346,8 @@ public class Settings { return this.blockchainConfig; } - public boolean useBitcoinTestNet() { - return this.useBitcoinTestNet; + public BitcoinNet getBitcoinNet() { + return this.bitcoinNet; } public Long getSlowQueryThreshold() { diff --git a/src/test/java/org/qora/test/apps/BuildCheckpoints.java b/src/test/java/org/qora/test/apps/BuildCheckpoints.java index 44869bb7..5b7f2d44 100644 --- a/src/test/java/org/qora/test/apps/BuildCheckpoints.java +++ b/src/test/java/org/qora/test/apps/BuildCheckpoints.java @@ -16,8 +16,6 @@ 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; @@ -33,7 +31,7 @@ public class BuildCheckpoints { final BlockChain chain = new BlockChain(params, store); final PeerGroup peerGroup = new PeerGroup(params, chain); - final InetAddress ipAddress = InetAddress.getLocalHost(); + final InetAddress ipAddress = InetAddress.getLoopbackAddress(); final PeerAddress peerAddress = new PeerAddress(params, ipAddress); peerGroup.addAddress(peerAddress); peerGroup.start(); diff --git a/src/test/java/org/qora/test/btcacct/BuildP2SH.java b/src/test/java/org/qora/test/btcacct/BuildP2SH.java new file mode 100644 index 00000000..fd2b9bf4 --- /dev/null +++ b/src/test/java/org/qora/test/btcacct/BuildP2SH.java @@ -0,0 +1,125 @@ +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.Coin; +import org.bitcoinj.core.LegacyAddress; +import org.bitcoinj.core.NetworkParameters; +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; + +public class BuildP2SH { + + private static void usage(String error) { + if (error != null) + System.err.println(error); + + System.err.println(String.format("usage: BuildP2SH ()")); + System.err.println(String.format("example: BuildP2SH " + + "mrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n" + + "\t0.00008642 \\\n" + + "\tn2N5VKrzq39nmuefZwp3wBiF4icdXX2B6o \\\n" + + "\td1b64100879ad93ceaa3c15929b6fe8550f54967 \\\n" + + "\t1585920000")); + 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"); + + BTC btc = BTC.getInstance(); + NetworkParameters params = btc.getNetworkParameters(); + + Address refundBitcoinAddress = null; + Coin bitcoinAmount = null; + Address redeemBitcoinAddress = null; + byte[] secretHash = null; + int lockTime = 0; + Coin bitcoinFee = BTCACCT.DEFAULT_BTC_FEE; + + int argIndex = 0; + try { + refundBitcoinAddress = Address.fromString(params, args[argIndex++]); + if (refundBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH) + usage("Refund BTC address must be in P2PKH form"); + + bitcoinAmount = Coin.parseCoin(args[argIndex++]); + + redeemBitcoinAddress = Address.fromString(params, args[argIndex++]); + if (redeemBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH) + usage("Redeem BTC address must be in P2PKH form"); + + secretHash = HashCode.fromString(args[argIndex++]).asBytes(); + if (secretHash.length != 20) + usage("Hash of secret must be 20 bytes"); + + lockTime = Integer.parseInt(args[argIndex++]); + int refundTimeoutDelay = lockTime - (int) (System.currentTimeMillis() / 1000L); + if (refundTimeoutDelay < 600 || refundTimeoutDelay > 7 * 24 * 60 * 60) + usage("Locktime (seconds) should be at between 10 minutes and 1 week from now"); + + if (args.length > argIndex) + bitcoinFee = Coin.parseCoin(args[argIndex++]); + } catch (IllegalArgumentException e) { + usage(String.format("Invalid argument %d: %s", argIndex, 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("Refund Bitcoin address: %s", refundBitcoinAddress)); + System.out.println(String.format("Bitcoin redeem amount: %s", bitcoinAmount.toPlainString())); + + System.out.println(String.format("Redeem Bitcoin address: %s", redeemBitcoinAddress)); + System.out.println(String.format("Redeem miner's fee: %s", BTC.FORMAT.format(bitcoinFee))); + + System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime)); + System.out.println(String.format("Hash of secret: %s", HashCode.fromBytes(secretHash))); + + byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash); + 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 (includes redeem/refund fee of %s)", + p2shAddress.toString(), BTC.FORMAT.format(bitcoinAmount), BTC.FORMAT.format(bitcoinFee))); + + System.out.println("Once this is done, responder should run Respond to check P2SH funding and create AT"); + } catch (DataException e) { + throw new RuntimeException("Repository issue: " + e.getMessage()); + } + } + +} diff --git a/src/test/java/org/qora/test/btcacct/CheckP2SH.java b/src/test/java/org/qora/test/btcacct/CheckP2SH.java new file mode 100644 index 00000000..ac090e69 --- /dev/null +++ b/src/test/java/org/qora/test/btcacct/CheckP2SH.java @@ -0,0 +1,172 @@ +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.Coin; +import org.bitcoinj.core.LegacyAddress; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.TransactionOutput; +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; + +public class CheckP2SH { + + private static void usage(String error) { + if (error != null) + System.err.println(error); + + System.err.println(String.format("usage: CheckP2SH ()")); + System.err.println(String.format("example: CheckP2SH " + + "2NEZboTLhBDPPQciR7sExBhy3TsDi7wV3Cv \\\n" + + "mrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n" + + "\t0.00008642 \\\n" + + "\tn2N5VKrzq39nmuefZwp3wBiF4icdXX2B6o \\\n" + + "\td1b64100879ad93ceaa3c15929b6fe8550f54967 \\\n" + + "\t1585920000")); + System.exit(1); + } + + public static void main(String[] args) { + if (args.length < 6 || args.length > 7) + usage(null); + + Security.insertProviderAt(new BouncyCastleProvider(), 0); + Settings.fileInstance("settings-test.json"); + + BTC btc = BTC.getInstance(); + NetworkParameters params = btc.getNetworkParameters(); + + Address p2shAddress = null; + Address refundBitcoinAddress = null; + Coin bitcoinAmount = null; + Address redeemBitcoinAddress = null; + byte[] secretHash = null; + int lockTime = 0; + Coin bitcoinFee = BTCACCT.DEFAULT_BTC_FEE; + + int argIndex = 0; + try { + p2shAddress = Address.fromString(params, args[argIndex++]); + if (p2shAddress.getOutputScriptType() != ScriptType.P2SH) + usage("P2SH address invalid"); + + refundBitcoinAddress = Address.fromString(params, args[argIndex++]); + if (refundBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH) + usage("Refund BTC address must be in P2PKH form"); + + bitcoinAmount = Coin.parseCoin(args[argIndex++]); + + redeemBitcoinAddress = Address.fromString(params, args[argIndex++]); + if (redeemBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH) + usage("Redeem BTC address must be in P2PKH form"); + + secretHash = HashCode.fromString(args[argIndex++]).asBytes(); + if (secretHash.length != 20) + usage("Hash of secret must be 20 bytes"); + + lockTime = Integer.parseInt(args[argIndex++]); + int refundTimeoutDelay = lockTime - (int) (System.currentTimeMillis() / 1000L); + if (refundTimeoutDelay < 600 || refundTimeoutDelay > 7 * 24 * 60 * 60) + usage("Locktime (seconds) should be at between 10 minutes and 1 week from now"); + + if (args.length > argIndex) + bitcoinFee = Coin.parseCoin(args[argIndex++]); + } catch (IllegalArgumentException e) { + usage(String.format("Invalid argument %d: %s", argIndex, 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("Refund Bitcoin address: %s", redeemBitcoinAddress)); + System.out.println(String.format("Bitcoin redeem amount: %s", bitcoinAmount.toPlainString())); + + System.out.println(String.format("Redeem Bitcoin address: %s", refundBitcoinAddress)); + System.out.println(String.format("Redeem miner's fee: %s", BTC.FORMAT.format(bitcoinFee))); + + System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime)); + System.out.println(String.format("Hash of secret: %s", HashCode.fromBytes(secretHash))); + + System.out.println(String.format("P2SH address: %s", p2shAddress)); + + byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash); + 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); + } + + bitcoinAmount = bitcoinAmount.add(bitcoinFee); + + 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.out.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))); + + // Check P2SH is funded + final long startTime = lockTime - 86400; + + Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString(), startTime); + 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", p2shAddress, BTC.FORMAT.format(p2shBalance))); + + // Grab all P2SH funding transactions (just in case there are more than one) + List fundingOutputs = BTC.getInstance().getOutputs(p2shAddress.toString(), startTime); + if (fundingOutputs == null) { + System.err.println(String.format("Can't find outputs for P2SH")); + System.exit(2); + } + + System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : ""))); + + for (TransactionOutput fundingOutput : fundingOutputs) + System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), BTC.FORMAT.format(fundingOutput.getValue()))); + + 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); + } + } catch (DataException e) { + throw new RuntimeException("Repository issue: " + e.getMessage()); + } + } + +} diff --git a/src/test/java/org/qora/test/btcacct/DeployAT.java b/src/test/java/org/qora/test/btcacct/DeployAT.java new file mode 100644 index 00000000..da05c567 --- /dev/null +++ b/src/test/java/org/qora/test/btcacct/DeployAT.java @@ -0,0 +1,158 @@ +package org.qora.test.btcacct; + +import java.math.BigDecimal; +import java.security.Security; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.qora.account.PrivateKeyAccount; +import org.qora.asset.Asset; +import org.qora.controller.Controller; +import org.qora.crosschain.BTCACCT; +import org.qora.crypto.Crypto; +import org.qora.data.transaction.BaseTransactionData; +import org.qora.data.transaction.DeployAtTransactionData; +import org.qora.data.transaction.TransactionData; +import org.qora.group.Group; +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 org.qora.transaction.DeployAtTransaction; +import org.qora.transaction.Transaction; +import org.qora.transform.TransformationException; +import org.qora.transform.transaction.TransactionTransformer; +import org.qora.utils.Base58; + +import com.google.common.hash.HashCode; + +public class DeployAT { + + private static void usage(String error) { + if (error != null) + System.err.println(error); + + System.err.println(String.format("usage: DeployAT ()")); + System.err.println(String.format("example: DeployAT " + + "AdTd9SUEYSdTW8mgK3Gu72K97bCHGdUwi2VvLNjUohot \\\n" + + "\t3.1415 \\\n" + + "\tQgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v \\\n" + + "\td1b64100879ad93ceaa3c15929b6fe8550f54967 \\\n" + + "\t1585920000 \\\n" + + "\t0.0001")); + 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"); + + byte[] refundPrivateKey = null; + BigDecimal qortAmount = null; + String redeemAddress = null; + byte[] secretHash = null; + int lockTime = 0; + BigDecimal initialPayout = BigDecimal.ZERO.setScale(8); + + int argIndex = 0; + try { + refundPrivateKey = Base58.decode(args[argIndex++]); + if (refundPrivateKey.length != 32) + usage("Refund private key must be 32 bytes"); + + qortAmount = new BigDecimal(args[argIndex++]); + if (qortAmount.signum() <= 0) + usage("QORT amount must be positive"); + + redeemAddress = args[argIndex++]; + if (!Crypto.isValidAddress(redeemAddress)) + usage("Redeem address invalid"); + + secretHash = HashCode.fromString(args[argIndex++]).asBytes(); + if (secretHash.length != 20) + usage("Hash of secret must be 20 bytes"); + + lockTime = Integer.parseInt(args[argIndex++]); + + if (args.length > argIndex) + initialPayout = new BigDecimal(args[argIndex++]).setScale(8); + } catch (IllegalArgumentException e) { + usage(String.format("Invalid argument %d: %s", argIndex, 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:"); + + PrivateKeyAccount refundAccount = new PrivateKeyAccount(repository, refundPrivateKey); + System.out.println(String.format("Refund Qortal address: %s", refundAccount.getAddress())); + + System.out.println(String.format("QORT amount (INCLUDING FEES): %s", qortAmount.toPlainString())); + + System.out.println(String.format("HASH160 of secret: %s", HashCode.fromBytes(secretHash))); + + System.out.println(String.format("Redeem Qortal address: %s", redeemAddress)); + + // New/derived info + + System.out.println("\nCHECKING info from other party:"); + + System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneId.systemDefault()), lockTime)); + System.out.println("Make sure this is BEFORE P2SH lockTime to allow you to refund AT before P2SH refunded"); + + // Deploy AT + final int BLOCK_TIME = 60; // seconds + final int refundTimeout = (lockTime - (int) (System.currentTimeMillis() / 1000L)) / BLOCK_TIME; + + byte[] creationBytes = BTCACCT.buildQortalAT(secretHash, redeemAddress, refundTimeout, initialPayout); + System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString()); + + long txTimestamp = System.currentTimeMillis(); + byte[] lastReference = refundAccount.getLastReference(); + + if (lastReference == null) { + System.err.println(String.format("Qortal account %s has no last reference", refundAccount.getAddress())); + System.exit(2); + } + + BigDecimal fee = BigDecimal.ZERO; + BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, refundAccount.getPublicKey(), fee, null); + TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, "QORT-BTC", "QORT-BTC ACCT", "", "", creationBytes, qortAmount, Asset.QORT); + + Transaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData); + + fee = deployAtTransaction.calcRecommendedFee(); + deployAtTransactionData.setFee(fee); + + deployAtTransaction.sign(refundAccount); + + byte[] signedBytes = null; + try { + signedBytes = TransactionTransformer.toBytes(deployAtTransactionData); + } catch (TransformationException e) { + System.err.println(String.format("Unable to convert transaction to base58: %s", e.getMessage())); + System.exit(2); + } + + System.out.println(String.format("\nSigned transaction in base58, ready for POST /transactions/process:\n%s\n", Base58.encode(signedBytes))); + } catch (NumberFormatException e) { + usage(String.format("Number format exception: %s", e.getMessage())); + } catch (DataException e) { + throw new RuntimeException("Repository issue: " + e.getMessage()); + } + } + +} diff --git a/src/test/java/org/qora/test/btcacct/Initiate.java b/src/test/java/org/qora/test/btcacct/Initiate.java index d081f4ba..e5f185f4 100644 --- a/src/test/java/org/qora/test/btcacct/Initiate.java +++ b/src/test/java/org/qora/test/btcacct/Initiate.java @@ -121,7 +121,7 @@ public class Initiate { 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); + byte[] redeemScriptBytes = null; // 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); diff --git a/src/test/java/org/qora/test/btcacct/Redeem.java b/src/test/java/org/qora/test/btcacct/Redeem.java index 503fc710..fd2a1061 100644 --- a/src/test/java/org/qora/test/btcacct/Redeem.java +++ b/src/test/java/org/qora/test/btcacct/Redeem.java @@ -4,18 +4,16 @@ import java.security.Security; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneOffset; +import java.util.Arrays; 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; @@ -30,25 +28,6 @@ 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 { @@ -56,16 +35,17 @@ public class Redeem { System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager"); } - private static final long REFUND_TIMEOUT = 600L; // seconds - private static void usage(String error) { if (error != null) System.err.println(error); - System.err.println(String.format("usage: Redeem ()")); - System.err.println(String.format("example: Redeem 032783606be32a3e639a33afe2b15f058708ab124f3b290d595ee954390a0c8559 \\\n" + System.err.println(String.format("usage: Redeem ()")); + System.err.println(String.format("example: Redeem " + + "2NEZboTLhBDPPQciR7sExBhy3TsDi7wV3Cv \\\n" + "\tmrTDPdM15cFWJC4g223BXX5snicfVJBx6M \\\n" - + "\teb95e1c1a5e9e6733549faec85b71f74f67638ea63b0acf2f077e9d0cb94dfe8 1575653814 2Mtn4aLjjWVEWckdoTMK7P8WbkXJf1ES6yL")); + + "\tec199a4abc9d3bf024349e397535dfee9d287e174aeabae94237eb03a0118c03 \\\n" + + "\t736563726574 \\\n" + + "\t1585920000")); System.exit(1); } @@ -75,39 +55,44 @@ public class Redeem { 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; + BTC btc = BTC.getInstance(); + NetworkParameters params = btc.getNetworkParameters(); + Address p2shAddress = null; + Address refundBitcoinAddress = null; + byte[] redeemPrivateKey = null; + byte[] secret = null; + int lockTime = 0; Coin bitcoinFee = BTCACCT.DEFAULT_BTC_FEE; + int argIndex = 0; 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"); + refundBitcoinAddress = Address.fromString(params, args[argIndex++]); + if (refundBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH) + usage("Refund BTC address must be in P2PKH form"); + + redeemPrivateKey = HashCode.fromString(args[argIndex++]).asBytes(); + // Auto-trim + if (redeemPrivateKey.length >= 37 && redeemPrivateKey.length <= 38) + redeemPrivateKey = Arrays.copyOfRange(redeemPrivateKey, 1, 33); + if (redeemPrivateKey.length != 32) + usage("Redeem private key must be 32 bytes"); + + secret = HashCode.fromString(args[argIndex++]).asBytes(); + if (secret.length == 0) + usage("Invalid secret bytes"); + + lockTime = Integer.parseInt(args[argIndex++]); + if (args.length > argIndex) bitcoinFee = Coin.parseCoin(args[argIndex++]); - } catch (NumberFormatException | AddressFormatException e) { - usage(String.format("Argument format exception: %s", e.getMessage())); + } catch (IllegalArgumentException e) { + usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); } try { @@ -120,21 +105,22 @@ public class Redeem { 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 PRIVATE key: %s", HashCode.fromBytes(redeemPrivateKey))); + System.out.println(String.format("Redeem miner's fee: %s", BTC.FORMAT.format(bitcoinFee))); 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:"); + byte[] secretHash = BTC.hash160(secret); + System.out.println(String.format("HASH160 of secret: %s", HashCode.fromBytes(secretHash))); - ECKey tradeKey = ECKey.fromPrivate(tradePrivateKey); - System.out.println(String.format("Trade pubkeyhash: %s", HashCode.fromBytes(tradeKey.getPubKeyHash()))); + ECKey redeemKey = ECKey.fromPrivate(redeemPrivateKey); + Address redeemAddress = Address.fromKey(params, redeemKey, ScriptType.P2PKH); + System.out.println(String.format("Redeem recipient (PKH): %s (%s)", redeemAddress, HashCode.fromBytes(redeemAddress.getHash()))); - byte[] redeemScriptBytes = BTCACCT.buildScript(tradeKey.getPubKeyHash(), theirBitcoinAddress.getHash(), yourBitcoinKey.getPubKeyHash(), lockTime); + System.out.println(String.format("P2SH address: %s", p2shAddress)); + + byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash); System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes))); byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes); @@ -159,7 +145,10 @@ public class Redeem { System.exit(2); } - Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString(), lockTime - REFUND_TIMEOUT); + // Check P2SH is funded + final long startTime = ((int) (System.currentTimeMillis() / 1000L)) - 86400; + + Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString(), startTime); if (p2shBalance == null) { System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress)); System.exit(2); @@ -167,8 +156,16 @@ public class Redeem { System.out.println(String.format("P2SH address %s balance: %s BTC", p2shAddress, p2shBalance.toPlainString())); // Grab all P2SH funding transactions (just in case there are more than one) - List fundingOutputs = BTC.getInstance().getOutputs(p2shAddress.toString(), lockTime - REFUND_TIMEOUT); - System.out.println(String.format("Found %d unspent output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : ""))); + List fundingOutputs = BTC.getInstance().getOutputs(p2shAddress.toString(), startTime); + if (fundingOutputs == null) { + System.err.println(String.format("Can't find outputs for P2SH")); + System.exit(2); + } + + System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : ""))); + + for (TransactionOutput fundingOutput : fundingOutputs) + System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), BTC.FORMAT.format(fundingOutput.getValue()))); if (fundingOutputs.isEmpty()) { System.err.println(String.format("Can't redeem spent/unfunded P2SH")); @@ -184,7 +181,9 @@ public class Redeem { 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); + System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.FORMAT.format(redeemAmount), BTC.FORMAT.format(bitcoinFee))); + + Transaction redeemTransaction = BTCACCT.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutput, redeemScriptBytes, secret); byte[] redeemBytes = redeemTransaction.bitcoinSerialize(); diff --git a/src/test/java/org/qora/test/btcacct/Refund.java b/src/test/java/org/qora/test/btcacct/Refund.java index 662bf63e..3969c590 100644 --- a/src/test/java/org/qora/test/btcacct/Refund.java +++ b/src/test/java/org/qora/test/btcacct/Refund.java @@ -4,18 +4,16 @@ import java.security.Security; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneOffset; +import java.util.Arrays; 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; @@ -55,16 +53,17 @@ public class Refund { 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: Refund ()")); - System.err.println(String.format("example: Refund 03aa20871c2195361f2826c7a649eab6b42639630c4d8c33c55311d5c1e476b5d6 \\\n" + System.err.println(String.format("usage: Refund ()")); + System.err.println(String.format("example: Refund " + + "2NEZboTLhBDPPQciR7sExBhy3TsDi7wV3Cv \\\n" + + "\tef027fb5828c5e201eaf6de4cd3b0b340d16a191ef848cd691f35ef8f727358c9c01b576fb7e \\\n" + "\tn2N5VKrzq39nmuefZwp3wBiF4icdXX2B6o \\\n" - + "\teb95e1c1a5e9e6733549faec85b71f74f67638ea63b0acf2f077e9d0cb94dfe8 1575653814 2Mtn4aLjjWVEWckdoTMK7P8WbkXJf1ES6yL")); + + "\td1b64100879ad93ceaa3c15929b6fe8550f54967 \\\n" + + "\t1585920000")); System.exit(1); } @@ -74,39 +73,44 @@ public class Refund { 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; + BTC btc = BTC.getInstance(); + NetworkParameters params = btc.getNetworkParameters(); + Address p2shAddress = null; + byte[] refundPrivateKey = null; + Address redeemBitcoinAddress = null; + byte[] secretHash = null; + int lockTime = 0; Coin bitcoinFee = BTCACCT.DEFAULT_BTC_FEE; + int argIndex = 0; 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"); + refundPrivateKey = HashCode.fromString(args[argIndex++]).asBytes(); + // Auto-trim + if (refundPrivateKey.length >= 37 && refundPrivateKey.length <= 38) + refundPrivateKey = Arrays.copyOfRange(refundPrivateKey, 1, 33); + if (refundPrivateKey.length != 32) + usage("Refund private key must be 32 bytes"); + + redeemBitcoinAddress = Address.fromString(params, args[argIndex++]); + if (redeemBitcoinAddress.getOutputScriptType() != ScriptType.P2PKH) + usage("Their BTC address must be in P2PKH form"); + + secretHash = HashCode.fromString(args[argIndex++]).asBytes(); + if (secretHash.length != 20) + usage("HASH160 of secret must be 20 bytes"); + + lockTime = Integer.parseInt(args[argIndex++]); + if (args.length > argIndex) bitcoinFee = Coin.parseCoin(args[argIndex++]); - } catch (NumberFormatException | AddressFormatException e) { - usage(String.format("Argument format exception: %s", e.getMessage())); + } catch (IllegalArgumentException e) { + usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); } try { @@ -119,21 +123,21 @@ public class Refund { 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("Refund PRIVATE key: %s", HashCode.fromBytes(refundPrivateKey))); + System.out.println(String.format("Redeem Bitcoin address: %s", redeemBitcoinAddress)); 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())); + System.out.println(String.format("Refund miner's fee: %s", BTC.FORMAT.format(bitcoinFee))); // 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()))); + ECKey refundKey = ECKey.fromPrivate(refundPrivateKey); + Address refundAddress = Address.fromKey(params, refundKey, ScriptType.P2PKH); + System.out.println(String.format("Refund recipient (PKH): %s (%s)", refundAddress, HashCode.fromBytes(refundAddress.getHash()))); - byte[] redeemScriptBytes = BTCACCT.buildScript(tradeKey.getPubKeyHash(), yourBitcoinKey.getPubKeyHash(), theirBitcoinAddress.getHash(), lockTime); + byte[] redeemScriptBytes = BTCACCT.buildScript(refundAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash); System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes))); byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes); @@ -163,7 +167,10 @@ public class Refund { System.exit(2); } - Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString(), lockTime - REFUND_TIMEOUT); + // Check P2SH is funded + final long startTime = ((int) (System.currentTimeMillis() / 1000L)) - 86400; + + Coin p2shBalance = BTC.getInstance().getBalance(p2shAddress.toString(), startTime); if (p2shBalance == null) { System.err.println(String.format("Unable to check P2SH address %s balance", p2shAddress)); System.exit(2); @@ -171,8 +178,16 @@ public class Refund { System.out.println(String.format("P2SH address %s balance: %s BTC", p2shAddress, p2shBalance.toPlainString())); // Grab all P2SH funding transactions (just in case there are more than one) - List fundingOutputs = BTC.getInstance().getOutputs(p2shAddress.toString(), lockTime - REFUND_TIMEOUT); - System.out.println(String.format("Found %d unspent output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : ""))); + List fundingOutputs = BTC.getInstance().getOutputs(p2shAddress.toString(), startTime); + if (fundingOutputs == null) { + System.err.println(String.format("Can't find outputs for P2SH")); + System.exit(2); + } + + System.out.println(String.format("Found %d output%s for P2SH", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : ""))); + + for (TransactionOutput fundingOutput : fundingOutputs) + System.out.println(String.format("Output %s:%d amount %s", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), BTC.FORMAT.format(fundingOutput.getValue()))); if (fundingOutputs.isEmpty()) { System.err.println(String.format("Can't refund spent/unfunded P2SH")); @@ -188,11 +203,13 @@ public class Refund { 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); + System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.FORMAT.format(refundAmount), BTC.FORMAT.format(bitcoinFee))); - byte[] refundBytes = refundTransaction.bitcoinSerialize(); + Transaction redeemTransaction = BTCACCT.buildRefundTransaction(refundAmount, refundKey, fundingOutput, redeemScriptBytes, lockTime); - System.out.println(String.format("\nLoad this transaction into your wallet and broadcast:\n%s\n", HashCode.fromBytes(refundBytes).toString())); + 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) { diff --git a/src/test/java/org/qora/test/btcacct/Respond2.java b/src/test/java/org/qora/test/btcacct/Respond2.java index 5699ad1a..f7f5ccbc 100644 --- a/src/test/java/org/qora/test/btcacct/Respond2.java +++ b/src/test/java/org/qora/test/btcacct/Respond2.java @@ -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.buildScript(secretHash, theirBitcoinPubKey, yourBitcoinPubKey, lockTime); + byte[] redeemScriptBytes = null; // BTCACCT.buildScript(secretHash, theirBitcoinPubKey, yourBitcoinPubKey, lockTime); System.out.println("Redeem script: " + HashCode.fromBytes(redeemScriptBytes).toString()); byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes); @@ -166,7 +166,7 @@ public class Respond2 { System.out.println("\nYour response:"); // If good, deploy AT - byte[] creationBytes = BTCACCT.buildCiyamAT(secretHash, theirQortPubKey, REFUND_TIMEOUT / 60); + byte[] creationBytes = null; // BTCACCT.buildQortalAT(secretHash, theirQortPubKey, REFUND_TIMEOUT / 60); System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString()); BigDecimal qortAmount = new BigDecimal(rawQortAmount).setScale(8);