diff --git a/lib/org/ciyam/at/1.2/at-1.2.jar b/lib/org/ciyam/at/1.2/at-1.2.jar index 862c37c6..daa4c474 100644 Binary files a/lib/org/ciyam/at/1.2/at-1.2.jar and b/lib/org/ciyam/at/1.2/at-1.2.jar differ diff --git a/lib/org/ciyam/at/maven-metadata-local.xml b/lib/org/ciyam/at/maven-metadata-local.xml index ccf3a2dd..65fd3fa6 100644 --- a/lib/org/ciyam/at/maven-metadata-local.xml +++ b/lib/org/ciyam/at/maven-metadata-local.xml @@ -8,6 +8,6 @@ 1.0 1.2 - 20191120104937 + 20191121173210 diff --git a/pom.xml b/pom.xml index c9da9284..067c359b 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ 1.0.6 jar - 0.15.4 + 0.15.5 1.64 ${maven.build.timestamp} 3.6 @@ -406,13 +406,13 @@ org.ciyam at - 1.0 + 1.2 org.bitcoinj bitcoinj-core - ${bitcoin.version} + ${bitcoinj.version} diff --git a/src/main/java/org/qora/crosschain/BTCACCT.java b/src/main/java/org/qora/crosschain/BTCACCT.java index 297c6364..8cb6f062 100644 --- a/src/main/java/org/qora/crosschain/BTCACCT.java +++ b/src/main/java/org/qora/crosschain/BTCACCT.java @@ -3,6 +3,18 @@ package org.qora.crosschain; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.Sha256Hash; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionOutPoint; +import org.bitcoinj.script.Script; +import org.bitcoinj.script.ScriptBuilder; +import org.bitcoinj.wallet.Wallet; +import org.bitcoinj.script.Script.ScriptType; import org.ciyam.at.FunctionCode; import org.ciyam.at.MachineState; import org.ciyam.at.OpCode; @@ -40,7 +52,7 @@ public class BTCACCT { * @param lockTime * @return */ - public static byte[] buildRedeemScript(byte[] secretHash, byte[] senderPubKey, byte[] recipientPubKey, long lockTime) { + public static byte[] buildRedeemScript(byte[] secretHash, byte[] senderPubKey, byte[] recipientPubKey, int lockTime) { byte[] senderPubKeyHash160 = BTC.hash160(senderPubKey); byte[] recipientPubKeyHash160 = BTC.hash160(recipientPubKey); diff --git a/src/test/java/org/qora/test/btcacct/Initiate1.java b/src/test/java/org/qora/test/btcacct/Initiate1.java index 859a7d2e..d04f2a41 100644 --- a/src/test/java/org/qora/test/btcacct/Initiate1.java +++ b/src/test/java/org/qora/test/btcacct/Initiate1.java @@ -1,19 +1,39 @@ package org.qora.test.btcacct; +import java.io.File; +import java.math.BigDecimal; import java.security.SecureRandom; import java.security.Security; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; import org.bitcoinj.core.Address; +import org.bitcoinj.core.Coin; import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.InsufficientMoneyException; import org.bitcoinj.core.LegacyAddress; import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionBroadcast; +import org.bitcoinj.kits.WalletAppKit; import org.bitcoinj.params.TestNet3Params; import org.bitcoinj.script.Script.ScriptType; +import org.bitcoinj.wallet.SendRequest; import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.qora.account.PrivateKeyAccount; import org.qora.account.PublicKeyAccount; +import org.qora.controller.Controller; import org.qora.crosschain.BTC; import org.qora.crosschain.BTCACCT; import org.qora.crypto.Crypto; +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.utils.Base58; import com.google.common.hash.HashCode; @@ -41,8 +61,8 @@ public class Initiate1 { private static final long REFUND_TIMEOUT = 600L; // seconds private static void usage() { - System.err.println(String.format("usage: Initiate1 ")); - System.err.println(String.format("example: Initiate1 6rNn9b3pYRrG9UKqzMWYZ9qa8F3Zgv2mVWrULGHUusb \\\n" + System.err.println(String.format("usage: Initiate1 ")); + System.err.println(String.format("example: Initiate1 pYQ6DpQBJ2n72TCLJLScEvwhf3boxWy2kQEPynakwpj \\\n" + "\t03aa20871c2195361f2826c7a649eab6b42639630c4d8c33c55311d5c1e476b5d6 \\\n" + "\t123 0.00008642 \\\n" + "\tJBNBQQDzZsm5do1BrwWAp53Ps4KYJVt749EGpCf7ofte \\\n" @@ -57,16 +77,29 @@ public class Initiate1 { Security.insertProviderAt(new BouncyCastleProvider(), 0); NetworkParameters params = TestNet3Params.get(); - String yourQortPubKey58 = args[0]; - String yourBitcoinPubKeyHex = args[1]; + int argIndex = 0; + String yourQortPrivKey58 = args[argIndex++]; + String yourBitcoinPubKeyHex = args[argIndex++]; - String theirBitcoinPubKeyHex = args[5]; + String rawQortAmount = args[argIndex++]; + String rawBitcoinAmount = args[argIndex++]; + + String theirQortPubKey58 = args[argIndex++]; + String theirBitcoinPubKeyHex = args[argIndex++]; 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:"); - byte[] yourQortPubKey = Base58.decode(yourQortPubKey58); - PublicKeyAccount yourQortalAccount = new PublicKeyAccount(null, yourQortPubKey); + byte[] yourQortPrivKey = Base58.decode(yourQortPrivKey58); + PrivateKeyAccount yourQortalAccount = new PrivateKeyAccount(repository, yourQortPrivKey); + byte[] yourQortPubKey = yourQortalAccount.getPublicKey(); System.out.println(String.format("Your Qortal address: %s", yourQortalAccount.getAddress())); byte[] yourBitcoinPubKey = HashCode.fromString(yourBitcoinPubKeyHex).asBytes(); @@ -74,6 +107,10 @@ public class Initiate1 { Address yourBitcoinAddress = Address.fromKey(params, yourBitcoinKey, ScriptType.P2PKH); System.out.println(String.format("Your Bitcoin address: %s", yourBitcoinAddress.toString())); + byte[] theirQortPubKey = Base58.decode(theirQortPubKey58); + PublicKeyAccount theirQortalAccount = new PublicKeyAccount(repository, theirQortPubKey); + System.out.println(String.format("Their Qortal address: %s", theirQortalAccount.getAddress())); + byte[] theirBitcoinPubKey = HashCode.fromString(theirBitcoinPubKeyHex).asBytes(); ECKey theirBitcoinKey = ECKey.fromPublicOnly(theirBitcoinPubKey); Address theirBitcoinAddress = Address.fromKey(params, theirBitcoinKey, ScriptType.P2PKH); @@ -91,7 +128,9 @@ public class Initiate1 { byte[] secretHash = Crypto.digest(secret); System.out.println("Hash of secret: " + HashCode.fromBytes(secretHash).toString()); - long lockTime = System.currentTimeMillis() + REFUND_TIMEOUT; + int lockTime = (int) ((System.currentTimeMillis() / 1000L) + REFUND_TIMEOUT); + System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneId.systemDefault()).toString(), lockTime)); + byte[] redeemScriptBytes = BTCACCT.buildRedeemScript(secretHash, yourBitcoinPubKey, theirBitcoinPubKey, lockTime); System.out.println("Redeem script: " + HashCode.fromBytes(redeemScriptBytes).toString()); @@ -99,8 +138,17 @@ public class Initiate1 { Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); System.out.println("P2SH address: " + p2shAddress.toString()); + + Coin bitcoinAmount = Coin.parseCoin(rawBitcoinAmount); + + // Fund P2SH + System.out.println(String.format("\nYou need to fund %s with %s BTC", p2shAddress.toString(), bitcoinAmount.toPlainString())); + + System.out.println("Once this is done, responder should run Respond2 to check P2SH funding and create AT"); } catch (NumberFormatException e) { usage(); + } catch (DataException e) { + throw new RuntimeException("Repository issue: " + e.getMessage()); } } diff --git a/src/test/java/org/qora/test/btcacct/Respond2.java b/src/test/java/org/qora/test/btcacct/Respond2.java new file mode 100644 index 00000000..89937fb4 --- /dev/null +++ b/src/test/java/org/qora/test/btcacct/Respond2.java @@ -0,0 +1,183 @@ +package org.qora.test.btcacct; + +import java.math.BigDecimal; +import java.security.SecureRandom; +import java.security.Security; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.LegacyAddress; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.params.TestNet3Params; +import org.bitcoinj.script.Script.ScriptType; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.qora.account.PrivateKeyAccount; +import org.qora.account.PublicKeyAccount; +import org.qora.asset.Asset; +import org.qora.controller.Controller; +import org.qora.crosschain.BTC; +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.transaction.DeployAtTransaction; +import org.qora.transaction.Transaction; +import org.qora.utils.Base58; + +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 recipient+secret+script as input to BTC P2SH address, releasing BTC amount - fees to responder + * + * Qora nodes scan for P2SH output, checks amount and recipient and if ok sends secret to Qora ACCT AT + * (Or it's possible to feed BTC transaction details into Qora AT so it can check them itself?) + * + * Qora ACCT AT sends its Qora to initiator + * + */ + +public class Respond2 { + + private static final long REFUND_TIMEOUT = 600L; // seconds + + private static void usage() { + System.err.println(String.format("usage: Respond2 ")); + System.err.println(String.format("example: Respond2 pYQ6DpQBJ2n72TCLJLScEvwhf3boxWy2kQEPynakwpj \\\n" + + "\t03aa20871c2195361f2826c7a649eab6b42639630c4d8c33c55311d5c1e476b5d6 \\\n" + + "\t123 0.00008642 \\\n" + + "\tJBNBQQDzZsm5do1BrwWAp53Ps4KYJVt749EGpCf7ofte \\\n" + + "\t032783606be32a3e639a33afe2b15f058708ab124f3b290d595ee954390a0c8559 \\\n" + + "\te43f5ab106b70df2e85656de30e1862891752f81e82f5dfd03abb8465a7346f9 1574441679 2N4R2pSEzLcJgtgAbFuLvviwwEkBrmq6sx4")); + System.exit(1); + } + + public static void main(String[] args) { + if (args.length != 9) + usage(); + + Security.insertProviderAt(new BouncyCastleProvider(), 0); + NetworkParameters params = TestNet3Params.get(); + + int argIndex = 0; + String yourQortPrivKey58 = args[argIndex++]; + String yourBitcoinPubKeyHex = args[argIndex++]; + + String rawQortAmount = args[argIndex++]; + String rawBitcoinAmount = args[argIndex++]; + + String theirQortPubKey58 = args[argIndex++]; + String theirBitcoinPubKeyHex = args[argIndex++]; + + String secretHashHex = args[argIndex++]; + String rawLockTime = args[argIndex++]; + String rawP2shAddress = args[argIndex++]; + + 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:"); + + byte[] yourQortPrivKey = Base58.decode(yourQortPrivKey58); + PrivateKeyAccount yourQortalAccount = new PrivateKeyAccount(repository, yourQortPrivKey); + byte[] yourQortPubKey = yourQortalAccount.getPublicKey(); + System.out.println(String.format("Your Qortal address: %s", yourQortalAccount.getAddress())); + + byte[] yourBitcoinPubKey = HashCode.fromString(yourBitcoinPubKeyHex).asBytes(); + ECKey yourBitcoinKey = ECKey.fromPublicOnly(yourBitcoinPubKey); + Address yourBitcoinAddress = Address.fromKey(params, yourBitcoinKey, ScriptType.P2PKH); + System.out.println(String.format("Your Bitcoin address: %s", yourBitcoinAddress.toString())); + + byte[] theirQortPubKey = Base58.decode(theirQortPubKey58); + PublicKeyAccount theirQortalAccount = new PublicKeyAccount(repository, theirQortPubKey); + System.out.println(String.format("Their Qortal address: %s", theirQortalAccount.getAddress())); + + byte[] theirBitcoinPubKey = HashCode.fromString(theirBitcoinPubKeyHex).asBytes(); + ECKey theirBitcoinKey = ECKey.fromPublicOnly(theirBitcoinPubKey); + Address theirBitcoinAddress = Address.fromKey(params, theirBitcoinKey, ScriptType.P2PKH); + System.out.println(String.format("Their Bitcoin address: %s", theirBitcoinAddress.toString())); + + System.out.println("Hash of secret: " + secretHashHex); + + // New/derived info + + System.out.println("\nCHECKING info from other party:"); + + int lockTime = Integer.valueOf(rawLockTime); + System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneId.systemDefault()).toString(), lockTime)); + + byte[] secretHash = HashCode.fromString(secretHashHex).asBytes(); + System.out.println("Hash of secret: " + HashCode.fromBytes(secretHash).toString()); + + byte[] redeemScriptBytes = BTCACCT.buildRedeemScript(secretHash, theirBitcoinPubKey, yourBitcoinPubKey, lockTime); + System.out.println("Redeem script: " + HashCode.fromBytes(redeemScriptBytes).toString()); + + byte[] redeemScriptHash = BTC.hash160(redeemScriptBytes); + + Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); + System.out.println("P2SH address: " + p2shAddress.toString()); + + if (!p2shAddress.toString().equals(rawP2shAddress)) { + System.err.println(String.format("Derived P2SH address %s does not match given address %s", p2shAddress.toString(), rawP2shAddress)); + System.exit(2); + } + + // TODO: Check for funded P2SH + + + System.out.println("\nYour response:"); + + // If good, deploy AT + byte[] creationBytes = BTCACCT.buildCiyamAT(secretHash, theirQortPubKey, REFUND_TIMEOUT / 60); + System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString()); + + BigDecimal qortAmount = new BigDecimal(rawQortAmount).setScale(8); + + long txTimestamp = System.currentTimeMillis(); + byte[] lastReference = yourQortalAccount.getLastReference(); + + if (lastReference == null) { + System.err.println(String.format("Qortal account %s has no last reference", yourQortalAccount.getAddress())); + System.exit(2); + } + + BigDecimal fee = BigDecimal.ZERO; + BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, yourQortPubKey, 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(yourQortalAccount); + } catch (NumberFormatException e) { + usage(); + } catch (DataException e) { + throw new RuntimeException("Repository issue: " + e.getMessage()); + } + } + +}