From bb4bdfede50bbe64978ae6dbd1e8f10623ef101f Mon Sep 17 00:00:00 2001 From: CalDescent Date: Fri, 27 May 2022 10:29:24 +0200 Subject: [PATCH] Added concept of a "disposable" pirate chain wallet. This is needed to allow redeem/refund of P2SH without having an actively synced and initialized wallet. It also ultimately avoids us having to retain the wallet entropy in the trade bot states. Various safety checks have been introduced to make sure that a disposable wallet is never used for anything other than P2SH redeem/refund. --- .../PirateChainWalletController.java | 22 +++++++- .../tradebot/PirateChainACCTv3TradeBot.java | 6 +-- .../org/qortal/crosschain/PirateChain.java | 31 ++++++----- .../org/qortal/crosschain/PirateWallet.java | 51 ++++++++++++++++--- 4 files changed, 83 insertions(+), 27 deletions(-) diff --git a/src/main/java/org/qortal/controller/PirateChainWalletController.java b/src/main/java/org/qortal/controller/PirateChainWalletController.java index 662e33c7..d12660e6 100644 --- a/src/main/java/org/qortal/controller/PirateChainWalletController.java +++ b/src/main/java/org/qortal/controller/PirateChainWalletController.java @@ -105,8 +105,9 @@ public class PirateChainWalletController extends Thread { } try { - this.currentWallet = new PirateWallet(entropyBytes); - if (!this.currentWallet.isReady()) { + this.currentWallet = new PirateWallet(entropyBytes, false); + if (!this.currentWallet.isReady() || this.currentWallet.isDisposable()) { + // Don't persist wallets that aren't ready or are disposable this.currentWallet = null; } return true; @@ -117,6 +118,16 @@ public class PirateChainWalletController extends Thread { return false; } + public PirateWallet switchToDisposableWallet() { + try { + this.currentWallet = null; + return new PirateWallet(null, true); + + } catch (IOException e) { + return null; + } + } + private void saveCurrentWallet() { if (this.currentWallet == null) { // Nothing to do @@ -149,6 +160,13 @@ public class PirateChainWalletController extends Thread { } } + public void ensureNotDisposable() throws ForeignBlockchainException { + // Safety check to make sure funds aren't sent to a disposable wallet + if (this.currentWallet == null || this.currentWallet.isDisposable()) { + throw new ForeignBlockchainException("Invalid wallet"); + } + } + public void ensureSynchronized() throws ForeignBlockchainException { String response = LiteWalletJni.execute("syncStatus", ""); JSONObject json = new JSONObject(response); diff --git a/src/main/java/org/qortal/controller/tradebot/PirateChainACCTv3TradeBot.java b/src/main/java/org/qortal/controller/tradebot/PirateChainACCTv3TradeBot.java index 4bccd1d9..365ed61f 100644 --- a/src/main/java/org/qortal/controller/tradebot/PirateChainACCTv3TradeBot.java +++ b/src/main/java/org/qortal/controller/tradebot/PirateChainACCTv3TradeBot.java @@ -777,8 +777,8 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot { String privateKey58 = Base58.encode(privateKey); String redeemScript58 = Base58.encode(redeemScriptA); - String txid = PirateChain.getInstance().redeemP2sh(tradeBotData.getForeignKey(), p2shAddressT3, - receivingAddress, redeemAmount.value, redeemScript58, fundingTxid58, secret58, privateKey58); + String txid = PirateChain.getInstance().redeemP2sh(p2shAddressT3, receivingAddress, redeemAmount.value, + redeemScript58, fundingTxid58, secret58, privateKey58); LOGGER.info("Redeem txid: {}", txid); break; } @@ -848,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(), p2shAddressT3, + String txid = PirateChain.getInstance().refundP2sh(p2shAddressT3, receivingAddress, refundAmount.value, redeemScript58, fundingTxid58, lockTimeA, privateKey58); LOGGER.info("Refund txid: {}", txid); break; diff --git a/src/main/java/org/qortal/crosschain/PirateChain.java b/src/main/java/org/qortal/crosschain/PirateChain.java index cdae44a1..44029e21 100644 --- a/src/main/java/org/qortal/crosschain/PirateChain.java +++ b/src/main/java/org/qortal/crosschain/PirateChain.java @@ -263,6 +263,7 @@ public class PirateChain extends Bitcoiny { walletController.initWithEntropy58(entropy58); walletController.ensureInitialized(); walletController.ensureSynchronized(); + walletController.ensureNotDisposable(); // Get balance String response = LiteWalletJni.execute("balance", ""); @@ -281,6 +282,7 @@ public class PirateChain extends Bitcoiny { walletController.initWithEntropy58(entropy58); walletController.ensureInitialized(); walletController.ensureSynchronized(); + walletController.ensureNotDisposable(); List transactions = new ArrayList<>(); @@ -330,6 +332,7 @@ public class PirateChain extends Bitcoiny { PirateChainWalletController walletController = PirateChainWalletController.getInstance(); walletController.initWithEntropy58(entropy58); walletController.ensureInitialized(); + walletController.ensureNotDisposable(); return walletController.getCurrentWallet().getWalletAddress(); } @@ -340,6 +343,7 @@ public class PirateChain extends Bitcoiny { walletController.initWithEntropy58(pirateChainSendRequest.entropy58); walletController.ensureInitialized(); walletController.ensureSynchronized(); + walletController.ensureNotDisposable(); // Unlock wallet walletController.getCurrentWallet().unlock(); @@ -386,6 +390,7 @@ public class PirateChain extends Bitcoiny { walletController.initWithEntropy58(entropy58); walletController.ensureInitialized(); walletController.ensureSynchronized(); + walletController.ensureNotDisposable(); // Unlock wallet walletController.getCurrentWallet().unlock(); @@ -426,16 +431,13 @@ public class PirateChain extends Bitcoiny { throw new ForeignBlockchainException("Something went wrong"); } - public String redeemP2sh(String entropy58, String p2shAddress, String receivingAddress, long amount, String redeemScript58, + public String redeemP2sh(String p2shAddress, String receivingAddress, long amount, String redeemScript58, String fundingTxid58, String secret58, String privateKey58) throws ForeignBlockchainException { - PirateChainWalletController walletController = PirateChainWalletController.getInstance(); - walletController.initWithEntropy58(entropy58); - walletController.ensureInitialized(); - walletController.ensureSynchronized(); - - // Unlock wallet - walletController.getCurrentWallet().unlock(); + PirateWallet wallet = PirateChainWalletController.getInstance().switchToDisposableWallet(); + if (wallet == null) { + throw new ForeignBlockchainException("Unable to initialize disposable Pirate Chain wallet"); + } // Build spend JSONObject txn = new JSONObject(); @@ -478,16 +480,13 @@ public class PirateChain extends Bitcoiny { throw new ForeignBlockchainException("Something went wrong"); } - public String refundP2sh(String entropy58, String p2shAddress, String receivingAddress, long amount, String redeemScript58, + public String refundP2sh(String p2shAddress, String receivingAddress, long amount, String redeemScript58, String fundingTxid58, int lockTime, String privateKey58) throws ForeignBlockchainException { - PirateChainWalletController walletController = PirateChainWalletController.getInstance(); - walletController.initWithEntropy58(entropy58); - walletController.ensureInitialized(); - walletController.ensureSynchronized(); - - // Unlock wallet - walletController.getCurrentWallet().unlock(); + PirateWallet wallet = PirateChainWalletController.getInstance().switchToDisposableWallet(); + if (wallet == null) { + throw new ForeignBlockchainException("Unable to initialize disposable Pirate Chain wallet"); + } // Build spend JSONObject txn = new JSONObject(); diff --git a/src/main/java/org/qortal/crosschain/PirateWallet.java b/src/main/java/org/qortal/crosschain/PirateWallet.java index c0ff10b0..89014338 100644 --- a/src/main/java/org/qortal/crosschain/PirateWallet.java +++ b/src/main/java/org/qortal/crosschain/PirateWallet.java @@ -24,11 +24,14 @@ import java.nio.file.StandardOpenOption; import java.util.Arrays; import java.util.Objects; +import static org.qortal.crosschain.PirateChain.DEFAULT_BIRTHDAY; + public class PirateWallet { protected static final Logger LOGGER = LogManager.getLogger(PirateWallet.class); private byte[] entropyBytes; + private final boolean isDisposable; private String seedPhrase; private boolean ready = false; @@ -36,13 +39,14 @@ public class PirateWallet { private final String saplingOutput64; private final String saplingSpend64; - private static String SERVER_URI = "https://lightd.pirate.black:443/"; - private static String COIN_PARAMS_RESOURCE = "piratechain/coinparams.json"; - private static String SAPLING_OUTPUT_RESOURCE = "piratechain/saplingoutput_base64"; - private static String SAPLING_SPEND_RESOURCE = "piratechain/saplingspend_base64"; + private final static String SERVER_URI = "https://lightd.pirate.black:443/"; + private final static String COIN_PARAMS_RESOURCE = "piratechain/coinparams.json"; + private final static String SAPLING_OUTPUT_RESOURCE = "piratechain/saplingoutput_base64"; + private final static String SAPLING_SPEND_RESOURCE = "piratechain/saplingspend_base64"; - public PirateWallet(byte[] entropyBytes) throws IOException { + public PirateWallet(byte[] entropyBytes, boolean isDisposable) throws IOException { this.entropyBytes = entropyBytes; + this.isDisposable = isDisposable; final URL paramsUrl = Resources.getResource(COIN_PARAMS_RESOURCE); this.params = Resources.toString(paramsUrl, StandardCharsets.UTF_8); @@ -60,6 +64,17 @@ public class PirateWallet { try { LiteWalletJni.initlogging(); + if (this.entropyBytes == null) { + if (this.isDisposable) { + // Generate disposable wallet + this.entropyBytes = new byte[32]; + } + else { + // Need entropy bytes for a non disposable wallet + return false; + } + } + // Pirate library uses base64 encoding String entropy64 = Base64.toBase64String(this.entropyBytes); @@ -75,8 +90,20 @@ public class PirateWallet { if (wallet == null) { // Wallet doesn't exist, so create a new one + int birthday = DEFAULT_BIRTHDAY; + if (this.isDisposable) { + try { + // Attempt to set birthday to the current block for disposable wallets + birthday = PirateChain.getInstance().blockchainProvider.getCurrentHeight(); + } + catch (ForeignBlockchainException e) { + // Use the default height + } + } + // Initialize new wallet - String outputSeedResponse = LiteWalletJni.initfromseed(SERVER_URI, this.params, inputSeedPhrase, "1886500", this.saplingOutput64, this.saplingSpend64); // Thread-safe. + String birthdayString = String.format("%d", birthday); + String outputSeedResponse = LiteWalletJni.initfromseed(SERVER_URI, this.params, inputSeedPhrase, birthdayString, this.saplingOutput64, this.saplingSpend64); // Thread-safe. JSONObject outputSeedJson = new JSONObject(outputSeedResponse); String outputSeedPhrase = null; if (outputSeedJson.has("seed")) { @@ -170,6 +197,10 @@ public class PirateWallet { LOGGER.info("Error: can't save wallet, because no wallet it initialized"); return false; } + if (this.isDisposable) { + LOGGER.info("Error: can't save disposable wallet"); + return false; + } // Encrypt first (will do nothing if already encrypted) this.encrypt(); @@ -198,6 +229,10 @@ public class PirateWallet { } public String load() throws IOException { + if (this.isDisposable) { + // Can't load disposable wallets + return null; + } Path walletPath = this.getCurrentWalletPath(); if (!Files.exists(walletPath)) { return null; @@ -286,6 +321,10 @@ public class PirateWallet { return null; } + public boolean isDisposable() { + return this.isDisposable; + } + public Boolean isEncrypted() { String response = LiteWalletJni.execute("encryptionstatus", ""); JSONObject json = new JSONObject(response);