CIYAM AT & cross-chain trading.

Bump CIYAM AT requirement to v1.3

Remove multi-blockchain AT aspect for now (BlockchainAPI).

For PUT_PREVIOUS_BLOCK_HASH_INTO_A we no longer use SHA256 to condense 64-byte block signature into 32 bytes.
Now we put block height into A1 and SHA192 of signature into A2 through A4.
This allows possible future lookup of block data using "block hash", with verification that it is the same block.

Some AT functions use "address in B" but sometimes we populate B with account's public key instead.
So the method "getAccountFromB" is smart and checks for an actual, textual address in B starting with 'Q', otherwise assumes B contains public key.

The Settings field "useBitcoinTestNet" (boolean) now replaced with "bitcoinNet" (String) with possible values MAIN (default), TEST3, REGTEST.
This allows for more varied development/testing scenarios.

Use correct Bitcoin nSequence value 0xFFFFFFFE for P2SH, i.e. enable locktime, disable RBF.

Roll REGTEST checkpoints file generator into main BTC class.

Yet another rewrite of Bitcoin P2SH scripts for BTC-QORT cross-chain trading.
Added associated test classes BuildP2SH, CheckP2SH, DeployAT (unfinished).
This commit is contained in:
catbref
2020-04-08 09:29:25 +01:00
parent 8844cc0076
commit 87bb9090f5
19 changed files with 943 additions and 461 deletions

View File

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

View File

@@ -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 <refund-BTC-P2PKH> <BTC-amount> <redeem-BTC-P2PKH> <HASH160-of-secret> <locktime> (<BTC-redeem/refund-fee>)"));
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());
}
}
}

View File

@@ -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 <P2SH-address> <refund-BTC-P2PKH> <BTC-amount> <redeem-BTC-P2PKH> <HASH160-of-secret> <locktime> (<BTC-redeem/refund-fee>)"));
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<TransactionOutput> 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());
}
}
}

View File

@@ -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 <your Qortal PRIVATE key> <QORT amount> <redeem Qortal address> <HASH160-of-secret> <locktime> (<initial QORT payout>)"));
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());
}
}
}

View File

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

View File

@@ -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 <your-BTC-pubkey> <their-BTC-P2PKH> <trade-PRIVATE-key> <locktime> <P2SH-address> (<BTC-redeem/refund-fee>)"));
System.err.println(String.format("example: Redeem 032783606be32a3e639a33afe2b15f058708ab124f3b290d595ee954390a0c8559 \\\n"
System.err.println(String.format("usage: Redeem <P2SH-address> <refund-BTC-P2PKH> <redeem-BTC-PRIVATE-key> <secret> <locktime> (<BTC-redeem/refund-fee>)"));
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<TransactionOutput> 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<TransactionOutput> 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();

View File

@@ -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 <your-BTC-pubkey> <their-BTC-P2PKH> <trade-PRIVATE-key> <locktime> <P2SH-address> (<BTC-redeem/refund-fee>)"));
System.err.println(String.format("example: Refund 03aa20871c2195361f2826c7a649eab6b42639630c4d8c33c55311d5c1e476b5d6 \\\n"
System.err.println(String.format("usage: Refund <P2SH-address> <refund-BTC-PRIVATE-KEY> <redeem-BTC-P2PKH> <HASH160-of-secret> <locktime> (<BTC-redeem/refund-fee>)"));
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<TransactionOutput> 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<TransactionOutput> 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) {

View File

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