From 6b53eb53842dbf6098b689c2390773f34d547b04 Mon Sep 17 00:00:00 2001 From: CalDescent Date: Mon, 23 May 2022 23:14:54 +0100 Subject: [PATCH] Pirate Chain uses the 'b' prefix for P2SH addresses, but the light wallet library is configured to use 't3' (from Zcash), so it's easiest to just derive a different prefix for each destination. This could be simplified by configuring the light wallet library to use the correct 'b' prefix, but this didn't work when first attempted. --- .../tradebot/PirateChainACCTv3TradeBot.java | 46 ++++++++++--------- .../qortal/controller/tradebot/TradeBot.java | 2 +- .../org/qortal/crosschain/PirateChain.java | 9 +++- .../test/crosschain/PirateChainTests.java | 32 +++++++++++++ 4 files changed, 65 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/qortal/controller/tradebot/PirateChainACCTv3TradeBot.java b/src/main/java/org/qortal/controller/tradebot/PirateChainACCTv3TradeBot.java index e9665b76..4bccd1d9 100644 --- a/src/main/java/org/qortal/controller/tradebot/PirateChainACCTv3TradeBot.java +++ b/src/main/java/org/qortal/controller/tradebot/PirateChainACCTv3TradeBot.java @@ -293,13 +293,13 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot { // P2SH-A to be funded byte[] redeemScriptBytes = PirateChainHTLC.buildScript(tradeForeignPublicKey, lockTimeA, crossChainTradeData.creatorForeignPKH, hashOfSecretA); - String p2shAddress = PirateChain.getInstance().deriveP2shAddress(redeemScriptBytes); + String p2shAddressT3 = PirateChain.getInstance().deriveP2shAddress(redeemScriptBytes); // Use t3 prefix when funding byte[] redeemScriptWithPrefixBytes = PirateChainHTLC.buildScriptWithPrefix(tradeForeignPublicKey, lockTimeA, crossChainTradeData.creatorForeignPKH, hashOfSecretA); String redeemScriptWithPrefix58 = Base58.encode(redeemScriptWithPrefixBytes); // Send to P2SH address try { - String txid = PirateChain.getInstance().fundP2SH(seed58, p2shAddress, amountA, redeemScriptWithPrefix58); + String txid = PirateChain.getInstance().fundP2SH(seed58, p2shAddressT3, amountA, redeemScriptWithPrefix58); LOGGER.info("fundingTxidHex: {}", txid); } catch (ForeignBlockchainException e) { @@ -329,7 +329,7 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot { } } - TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress)); + TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddressT3)); return ResponseResult.OK; } @@ -510,13 +510,13 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot { // Determine P2SH-A address and confirm funded byte[] redeemScriptA = PirateChainHTLC.buildScript(aliceForeignPublicKey, lockTimeA, tradeBotData.getTradeForeignPublicKey(), hashOfSecretA); - String p2shAddressA = pirateChain.deriveP2shAddress(redeemScriptA); + String p2shAddress = pirateChain.deriveP2shAddressBPrefix(redeemScriptA); // Use 'b' prefix when checking status long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout); long p2shFee = PirateChain.getInstance().getP2shFee(feeTimestamp); final long minimumAmountA = tradeBotData.getForeignAmount() + p2shFee; - PirateChainHTLC.Status htlcStatusA = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddressA, minimumAmountA); + PirateChainHTLC.Status htlcStatusA = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA); switch (htlcStatusA) { case UNFUNDED: @@ -528,7 +528,7 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot { case REDEEMED: // We've already redeemed this? TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_DONE, - () -> String.format("P2SH-A %s already spent? Assuming trade complete", p2shAddressA)); + () -> String.format("P2SH-A %s already spent? Assuming trade complete", p2shAddress)); return; case REFUND_IN_PROGRESS: @@ -600,13 +600,13 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot { // Refund P2SH-A if we've passed lockTime-A if (NTP.getTime() >= lockTimeA * 1000L) { byte[] redeemScriptA = PirateChainHTLC.buildScript(tradeBotData.getTradeForeignPublicKey(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret()); - String p2shAddressA = pirateChain.deriveP2shAddress(redeemScriptA); + String p2shAddress = pirateChain.deriveP2shAddressBPrefix(redeemScriptA); // Use 'b' prefix when checking status long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout); long p2shFee = PirateChain.getInstance().getP2shFee(feeTimestamp); long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee; - PirateChainHTLC.Status htlcStatusA = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddressA, minimumAmountA); + PirateChainHTLC.Status htlcStatusA = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA); switch (htlcStatusA) { case UNFUNDED: @@ -618,21 +618,21 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot { case REDEEMED: // Already redeemed? TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE, - () -> String.format("P2SH-A %s already spent? Assuming trade completed", p2shAddressA)); + () -> String.format("P2SH-A %s already spent? Assuming trade completed", p2shAddress)); return; case REFUND_IN_PROGRESS: case REFUNDED: TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED, - () -> String.format("P2SH-A %s already refunded. Trade aborted", p2shAddressA)); + () -> String.format("P2SH-A %s already refunded. Trade aborted", p2shAddress)); return; } TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A, () -> atData.getIsFinished() - ? String.format("AT %s cancelled. Refunding P2SH-A %s - aborting trade", tradeBotData.getAtAddress(), p2shAddressA) - : String.format("LockTime-A reached, refunding P2SH-A %s - aborting trade", p2shAddressA)); + ? String.format("AT %s cancelled. Refunding P2SH-A %s - aborting trade", tradeBotData.getAtAddress(), p2shAddress) + : String.format("LockTime-A reached, refunding P2SH-A %s - aborting trade", p2shAddress)); return; } @@ -735,7 +735,8 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot { byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo(); int lockTimeA = crossChainTradeData.lockTimeA; byte[] redeemScriptA = PirateChainHTLC.buildScript(crossChainTradeData.partnerForeignPKH, lockTimeA, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretA); - String p2shAddressA = pirateChain.deriveP2shAddress(redeemScriptA); + String p2shAddress = pirateChain.deriveP2shAddressBPrefix(redeemScriptA); // Use 'b' prefix when checking status + String p2shAddressT3 = pirateChain.deriveP2shAddress(redeemScriptA); // Use 't3' prefix when refunding // Fee for redeem/refund is subtracted from P2SH-A balance. long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout); @@ -743,7 +744,7 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot { long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee; String receivingAddress = Bech32.encode("zs", receivingAccountInfo); - PirateChainHTLC.Status htlcStatusA = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddressA, minimumAmountA); + PirateChainHTLC.Status htlcStatusA = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA); switch (htlcStatusA) { case UNFUNDED: @@ -763,7 +764,7 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot { case FUNDED: { // Get funding txid - String fundingTxidHex = PirateChainHTLC.getUnspentFundingTxid(pirateChain.getBlockchainProvider(), p2shAddressA, minimumAmountA); + String fundingTxidHex = PirateChainHTLC.getUnspentFundingTxid(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA); if (fundingTxidHex == null) { throw new ForeignBlockchainException("Missing funding txid when redeeming P2SH"); } @@ -776,7 +777,7 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot { String privateKey58 = Base58.encode(privateKey); String redeemScript58 = Base58.encode(redeemScriptA); - String txid = PirateChain.getInstance().redeemP2sh(tradeBotData.getForeignKey(), p2shAddressA, + String txid = PirateChain.getInstance().redeemP2sh(tradeBotData.getForeignKey(), p2shAddressT3, receivingAddress, redeemAmount.value, redeemScript58, fundingTxid58, secret58, privateKey58); LOGGER.info("Redeem txid: {}", txid); break; @@ -807,13 +808,14 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot { return; byte[] redeemScriptA = PirateChainHTLC.buildScript(tradeBotData.getTradeForeignPublicKey(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret()); - String p2shAddressA = pirateChain.deriveP2shAddress(redeemScriptA); + String p2shAddress = pirateChain.deriveP2shAddressBPrefix(redeemScriptA); // Use 'b' prefix when checking status + String p2shAddressT3 = pirateChain.deriveP2shAddress(redeemScriptA); // Use 't3' prefix when refunding // Fee for redeem/refund is subtracted from P2SH-A balance. long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout); long p2shFee = PirateChain.getInstance().getP2shFee(feeTimestamp); long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee; - PirateChainHTLC.Status htlcStatusA = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddressA, minimumAmountA); + PirateChainHTLC.Status htlcStatusA = PirateChainHTLC.determineHtlcStatus(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA); switch (htlcStatusA) { case UNFUNDED: @@ -825,7 +827,7 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot { case REDEEMED: // Too late! TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE, - () -> String.format("P2SH-A %s already spent!", p2shAddressA)); + () -> String.format("P2SH-A %s already spent!", p2shAddress)); return; case REFUND_IN_PROGRESS: @@ -834,7 +836,7 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot { case FUNDED:{ // Get funding txid - String fundingTxidHex = PirateChainHTLC.getUnspentFundingTxid(pirateChain.getBlockchainProvider(), p2shAddressA, minimumAmountA); + String fundingTxidHex = PirateChainHTLC.getUnspentFundingTxid(pirateChain.getBlockchainProvider(), p2shAddress, minimumAmountA); if (fundingTxidHex == null) { throw new ForeignBlockchainException("Missing funding txid when refunding P2SH"); } @@ -846,7 +848,7 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot { String redeemScript58 = Base58.encode(redeemScriptA); String receivingAddress = pirateChain.getWalletAddress(tradeBotData.getForeignKey()); - String txid = PirateChain.getInstance().refundP2sh(tradeBotData.getForeignKey(), p2shAddressA, + String txid = PirateChain.getInstance().refundP2sh(tradeBotData.getForeignKey(), p2shAddressT3, receivingAddress, refundAmount.value, redeemScript58, fundingTxid58, lockTimeA, privateKey58); LOGGER.info("Refund txid: {}", txid); break; @@ -854,7 +856,7 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot { } TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED, - () -> String.format("LockTime-A reached. Refunded P2SH-A %s. Trade aborted", p2shAddressA)); + () -> String.format("LockTime-A reached. Refunded P2SH-A %s. Trade aborted", p2shAddress)); } /** diff --git a/src/main/java/org/qortal/controller/tradebot/TradeBot.java b/src/main/java/org/qortal/controller/tradebot/TradeBot.java index f29b4466..d635a9e0 100644 --- a/src/main/java/org/qortal/controller/tradebot/TradeBot.java +++ b/src/main/java/org/qortal/controller/tradebot/TradeBot.java @@ -299,7 +299,7 @@ public class TradeBot implements Listener { return ECKey.fromPrivate(privateKey).getPubKey(); } - /*package*/ static byte[] generateSecret() { + /*package*/ public static byte[] generateSecret() { byte[] secret = new byte[32]; RANDOM.nextBytes(secret); return secret; diff --git a/src/main/java/org/qortal/crosschain/PirateChain.java b/src/main/java/org/qortal/crosschain/PirateChain.java index 0069ceaf..cdae44a1 100644 --- a/src/main/java/org/qortal/crosschain/PirateChain.java +++ b/src/main/java/org/qortal/crosschain/PirateChain.java @@ -243,13 +243,20 @@ public class PirateChain extends Bitcoiny { return walletKey != null && Base58.decode(walletKey).length == 32; } - /** Returns P2SH address using passed redeem script. */ + /** Returns 't3' prefixed P2SH address using passed redeem script. */ public String deriveP2shAddress(byte[] redeemScriptBytes) { Context.propagate(bitcoinjContext); byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); return LegacyZcashAddress.fromScriptHash(this.params, redeemScriptHash).toString(); } + /** Returns 'b' prefixed P2SH address using passed redeem script. */ + public String deriveP2shAddressBPrefix(byte[] redeemScriptBytes) { + Context.propagate(bitcoinjContext); + byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); + return LegacyAddress.fromScriptHash(this.params, redeemScriptHash).toString(); + } + public Long getWalletBalance(String entropy58) throws ForeignBlockchainException { synchronized (this) { PirateChainWalletController walletController = PirateChainWalletController.getInstance(); diff --git a/src/test/java/org/qortal/test/crosschain/PirateChainTests.java b/src/test/java/org/qortal/test/crosschain/PirateChainTests.java index 847d703d..9b67570c 100644 --- a/src/test/java/org/qortal/test/crosschain/PirateChainTests.java +++ b/src/test/java/org/qortal/test/crosschain/PirateChainTests.java @@ -9,7 +9,9 @@ import org.junit.After; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; +import org.qortal.controller.tradebot.TradeBot; import org.qortal.crosschain.*; +import org.qortal.crypto.Crypto; import org.qortal.repository.DataException; import org.qortal.test.common.Common; import org.qortal.transform.TransformationException; @@ -84,6 +86,36 @@ public class PirateChainTests extends Common { assertNotNull(rawTransaction); } + @Test + public void testDeriveP2SHAddressWithT3Prefix() { + byte[] creatorTradePrivateKey = TradeBot.generateTradePrivateKey(); + byte[] creatorTradeForeignPublicKey = TradeBot.deriveTradeForeignPublicKey(creatorTradePrivateKey); + byte[] tradePrivateKey = TradeBot.generateTradePrivateKey(); + byte[] tradeForeignPublicKey = TradeBot.deriveTradeForeignPublicKey(tradePrivateKey); + byte[] secretA = TradeBot.generateSecret(); + byte[] hashOfSecretA = Crypto.hash160(secretA); + int lockTime = 1653233550; + + byte[] redeemScriptBytes = PirateChainHTLC.buildScript(tradeForeignPublicKey, lockTime, creatorTradeForeignPublicKey, hashOfSecretA); + String p2shAddress = PirateChain.getInstance().deriveP2shAddress(redeemScriptBytes); + assertTrue(p2shAddress.startsWith("t3")); + } + + @Test + public void testDeriveP2SHAddressWithBPrefix() { + byte[] creatorTradePrivateKey = TradeBot.generateTradePrivateKey(); + byte[] creatorTradeForeignPublicKey = TradeBot.deriveTradeForeignPublicKey(creatorTradePrivateKey); + byte[] tradePrivateKey = TradeBot.generateTradePrivateKey(); + byte[] tradeForeignPublicKey = TradeBot.deriveTradeForeignPublicKey(tradePrivateKey); + byte[] secretA = TradeBot.generateSecret(); + byte[] hashOfSecretA = Crypto.hash160(secretA); + int lockTime = 1653233550; + + byte[] redeemScriptBytes = PirateChainHTLC.buildScript(tradeForeignPublicKey, lockTime, creatorTradeForeignPublicKey, hashOfSecretA); + String p2shAddress = PirateChain.getInstance().deriveP2shAddressBPrefix(redeemScriptBytes); + assertTrue(p2shAddress.startsWith("b")); + } + @Test public void testHTLCStatusFunded() throws ForeignBlockchainException { String p2shAddress = "ba6Q5HWrWtmfU2WZqQbrFdRYsafA45cUAt";