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);