diff --git a/src/main/java/org/qortal/controller/TradeBot.java b/src/main/java/org/qortal/controller/TradeBot.java index 4e32316c..91f23345 100644 --- a/src/main/java/org/qortal/controller/TradeBot.java +++ b/src/main/java/org/qortal/controller/TradeBot.java @@ -59,6 +59,38 @@ public class TradeBot { return instance; } + /** + * Creates a new trade-bot entry from the "Bob" viewpoint, i.e. OFFERing QORT in exchange for BTC. + *

+ * Generates: + *

+ * Derives: + * + * A Qortal AT is then constructed including the following as constants in the 'data segment': + * + * Returns a DEPLOY_AT transaction that needs to be signed and broadcast to the Qortal network. + *

+ * Trade-bot will wait for Bob's AT to be deployed before taking next step. + *

+ * @param repository + * @param tradeBotCreateRequest + * @return raw, unsigned DEPLOY_AT transaction + * @throws DataException + */ public static byte[] createTrade(Repository repository, TradeBotCreateRequest tradeBotCreateRequest) throws DataException { byte[] tradePrivateKey = generateTradePrivateKey(); byte[] secretB = generateSecret(); @@ -115,6 +147,44 @@ public class TradeBot { } } + /** + * Creates a trade-bot entry from the 'Alice' viewpoint, i.e. matching BTC to an existing offer. + *

+ * Requires a chosen trade offer from Bob, passed by crossChainTradeData + * and access to a Bitcoin wallet via xprv58. + *

+ * The crossChainTradeData contains the current trade offer state + * as extracted from the AT's data segment. + *

+ * Access to a funded wallet is via a Bitcoin BIP32 hierarchical deterministic key, + * passed via xprv58. + * This key will be stored in your node's database + * to allow trade-bot to create/fund the necessary P2SH transactions! + * However, due to the nature of BIP32 keys, it is possible to give the trade-bot + * only a subset of wallet access (see BIP32 for more details). + *

+ * As an example, the xprv58 can be extract from a legacy, password-less + * Electrum wallet by going to the console tab and entering:
+ * wallet.keystore.xprv
+ * which should result in a base58 string starting with either 'xprv' (for Bitcoin main-net) + * or 'tprv' for (Bitcoin test-net). + *

+ * It is envisaged that the value in xprv58 will actually come from a Qortal-UI-managed wallet. + *

+ * If sufficient funds are available, this method will actually fund the P2SH-A + * with the Bitcoin amount expected by 'Bob'. + *

+ * If the Bitcoin transaction is successfully broadcast to the network then the trade-bot entry + * is saved to the repository and the cross-chain trading process commences. + *

+ * Trade-bot will wait for P2SH-A to confirm before taking next step. + *

+ * @param repository + * @param crossChainTradeData chosen trade OFFER that Alice wants to match + * @param xprv58 funded wallet xprv in base58 + * @return true if P2SH-A funding transaction successfully broadcast to Bitcoin network, false otherwise + * @throws DataException + */ public static boolean startResponse(Repository repository, CrossChainTradeData crossChainTradeData, String xprv58) throws DataException { byte[] tradePrivateKey = generateTradePrivateKey(); byte[] secretA = generateSecret(); @@ -258,6 +328,11 @@ public class TradeBot { } } + /** + * Trade-bot is waiting for Bob's AT to deploy. + *

+ * If AT is deployed, then trade-bot's next step is to wait for MESSAGE from Alice. + */ private void handleBobWaitingForAtConfirm(Repository repository, TradeBotData tradeBotData) throws DataException { if (!repository.getATRepository().exists(tradeBotData.getAtAddress())) return; @@ -269,6 +344,22 @@ public class TradeBot { LOGGER.info(() -> String.format("AT %s confirmed ready. Waiting for trade message", tradeBotData.getAtAddress())); } + /** + * Trade-bot is waiting for Alice's P2SH-A to confirm. + *

+ * If P2SH-A is confirmed, then trade-bot's next step is to MESSAGE Bob's trade address with Alice's trade info. + *

+ * It is possible between broadcast and confirmation of P2SH-A funding transaction, that Bob has cancelled his trade offer. + * If this is detected then trade-bot's next step is to wait until P2SH-A can refund back to Alice. + *

+ * In normal operation, trade-bot send a zero-fee, PoW MESSAGE on Alice's behalf containing: + *

+ * If MESSAGE transaction is successfully broadcast, trade-bot's next step is to wait until Bob's AT has locked trade to Alice only. + */ private void handleAliceWaitingForP2shA(Repository repository, TradeBotData tradeBotData) throws DataException { ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress()); if (atData == null) { @@ -328,6 +419,24 @@ public class TradeBot { p2shAddress, crossChainTradeData.qortalCreatorTradeAddress, tradeBotData.getAtAddress())); } + /** + * Trade-bot is waiting for MESSAGE from Alice's trade-bot, containing Alice's trade info. + *

+ * It's possible Bob has cancelling his trade offer, receiving an automatic QORT refund, + * in which case trade-bot is done with this specific trade and finalizes on refunded state. + *

+ * Assuming trade is still on offer, trade-bot checks the contents of MESSAGE from Alice's trade-bot. + *

+ * Details from Alice are used to derive P2SH-A address and this is checked for funding balance. + *

+ * Assuming P2SH-A has at least expected Bitcoin balance, + * Bob's trade-bot constructs a zero-fee, PoW MESSAGE to send to Bob's AT with more trade details. + *

+ * On processing this MESSAGE, Bob's AT should switch into 'TRADE' mode and only trade with Alice. + *

+ * Trade-bot's next step is to wait for P2SH-B, which will allow Bob to reveal his secret-B, + * needed by Alice to progress her side of the trade. + */ private void handleBobWaitingForMessage(Repository repository, TradeBotData tradeBotData) throws DataException { // Fetch AT so we can determine trade start timestamp ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress()); @@ -426,6 +535,20 @@ public class TradeBot { } } + /** + * Trade-bot is waiting for Bob's AT to switch to TRADE mode and lock trade to Alice only. + *

+ * It's possible that Bob has cancelled his trade offer in the mean time, or that somehow + * this process has taken so long that we've reached P2SH-A's locktime, or that someone else + * has managed to trade with Bob. In any of these cases, trade-bot switches to begin the refunding process. + *

+ * Assuming Bob's AT is locked to Alice, trade-bot checks AT's state data to make sure it is correct. + *

+ * If all is well, trade-bot then uses Bitcoin wallet to (token) fund P2SH-B. + *

+ * If P2SH-B funding transaction is successfully broadcast to the Bitcoin network, trade-bot's next + * step is to watch for Bob revealing secret-B by redeeming P2SH-B. + */ private void handleAliceWaitingForAtLock(Repository repository, TradeBotData tradeBotData) throws DataException { ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress()); if (atData == null) { @@ -527,6 +650,16 @@ public class TradeBot { tradeBotData.getAtAddress(), tradeBotData.getTradeNativeAddress(), p2shAddress)); } + /** + * Trade-bot is waiting for P2SH-B to funded. + *

+ * It's possible than Bob's AT has reached it's trading timeout and automatically refunded QORT back to Bob. + * In which case, trade-bot is done with this specific trade and finalizes on refunded state. + *

+ * Assuming P2SH-B is funded, trade-bot 'redeems' this P2SH using secret-B, thus revealing it to Alice. + *

+ * Trade-bot's next step is to wait for Alice to use secret-B, and her secret-A, to redeem Bob's AT. + */ private void handleBobWaitingForP2shB(Repository repository, TradeBotData tradeBotData) throws DataException { ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress()); if (atData == null) { @@ -583,6 +716,22 @@ public class TradeBot { LOGGER.info(() -> String.format("P2SH-B %s redeemed (exposing secret-B). Watching AT %s for secret-A", p2shAddress, tradeBotData.getAtAddress())); } + /** + * Trade-bot is waiting for Bob to redeem P2SH-B thus revealing secret-B to Alice. + *

+ * It's possible that this process has taken so long that we've reached P2SH-B's locktime. + * In which case, trade-bot switches to begin the refund process. + *

+ * If trade-bot can extract a valid secret-B from the spend of P2SH-B, then it creates a + * zero-fee, PoW MESSAGE to send to Bob's AT, including both secret-B and also Alice's secret-A. + *

+ * Both secrets are needed to release the QORT funds from Bob's AT to Alice's 'native'/Qortal + * trade address. + *

+ * In revealing a valid secret-A, Bob can then redeem the BTC funds from P2SH-A. + *

+ * If trade-bot successfully broadcasts the MESSAGE transaction, then this specific trade is done. + */ private void handleAliceWatchingP2shB(Repository repository, TradeBotData tradeBotData) throws DataException { ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress()); if (atData == null) { @@ -645,6 +794,19 @@ public class TradeBot { p2shAddress, tradeBotData.getAtAddress(), receiveAddress)); } + /** + * Trade-bot is waiting for Alice to redeem Bob's AT, thus revealing secret-A which is required to spend the BTC funds from P2SH-A. + *

+ * It's possible that Bob's AT has reached its trading timeout and automatically refunded QORT back to Bob. In which case, + * trade-bot is done with this specific trade and finalizes in refunded state. + *

+ * Assuming trade-bot can extract a valid secret-A from Alice's MESSAGE then trade-bot uses that to redeem the BTC funds from P2SH-A + * to Bob's 'foreign'/Bitcoin trade legacy-format address, as derived from trade private key. + *

+ * (This could potentially be 'improved' to send BTC to any address of Bob's choosing by changing the transaction output). + *

+ * If trade-bot successfully broadcasts the transaction, then this specific trade is done. + */ private void handleBobWaitingForAtRedeem(Repository repository, TradeBotData tradeBotData) throws DataException { ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress()); if (atData == null) { @@ -702,6 +864,13 @@ public class TradeBot { LOGGER.info(() -> String.format("P2SH-A %s redeemed. Funds should arrive at %s", tradeBotData.getAtAddress(), receiveAddress)); } + /** + * Trade-bot is attempting to refund P2SH-B. + *

+ * We could potentially skip this step as P2SH-B is only funded with a token amount to cover the mining fee should Bob redeem P2SH-B. + *

+ * Upon successful broadcast of P2SH-B refunding transaction, trade-bot's next step is to begin refunding of P2SH-A. + */ private void handleAliceRefundingP2shB(Repository repository, TradeBotData tradeBotData) throws DataException { ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress()); if (atData == null) { @@ -736,6 +905,7 @@ public class TradeBot { LOGGER.info(() -> String.format("Refunded P2SH-B %s. Waiting for LockTime-A", p2shAddress)); } + /** Trade-bot is attempting to refund P2SH-A. */ private void handleAliceRefundingP2shA(Repository repository, TradeBotData tradeBotData) throws DataException { ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress()); if (atData == null) { diff --git a/src/main/java/org/qortal/crosschain/BTCACCT.java b/src/main/java/org/qortal/crosschain/BTCACCT.java index 3910bfa4..cb87ca0f 100644 --- a/src/main/java/org/qortal/crosschain/BTCACCT.java +++ b/src/main/java/org/qortal/crosschain/BTCACCT.java @@ -77,11 +77,13 @@ import com.google.common.primitives.Bytes; *

  • Alice scans P2SH-b redeem tx to extract secret-b *
      *
    • Alice MESSAGEs Qortal AT from her trade address, sending secret-a & secret-b
    • + *
    • AT's QORT funds end up at Qortal address derived from Alice's trade private key
    • *
    *
  • *
  • Bob checks AT, extracts secret-a *
      *
    • Bob redeems P2SH-a using his Bitcoin trade key and secret-a
    • + *
    • P2SH-a funds end up in at Bitcoin address derived from Bob's trade private key
    • *
    *
  • * diff --git a/src/main/java/org/qortal/crosschain/ElectrumX.java b/src/main/java/org/qortal/crosschain/ElectrumX.java index d3541527..2331a305 100644 --- a/src/main/java/org/qortal/crosschain/ElectrumX.java +++ b/src/main/java/org/qortal/crosschain/ElectrumX.java @@ -29,6 +29,7 @@ import org.qortal.crypto.TrustlessSSLSocketFactory; import com.google.common.hash.HashCode; import com.google.common.primitives.Bytes; +/** ElectrumX network support for querying Bitcoin-related info like block headers, transaction outputs, etc. */ public class ElectrumX { private static final Logger LOGGER = LogManager.getLogger(ElectrumX.class); @@ -92,7 +93,22 @@ public class ElectrumX { private ElectrumX(String bitcoinNetwork) { switch (bitcoinNetwork) { case "MAIN": - servers.addAll(Arrays.asList()); + servers.addAll(Arrays.asList( + // Servers chosen on NO BASIS WHATSOEVER from various sources! + // new Server("tardis.bauerj.eu", Server.ConnectionType.SSL, 50002), + // new Server("rbx.curalle.ovh", Server.ConnectionType.SSL, 50002), + // new Server("quick.electumx.live", Server.ConnectionType.SSL, 50002), + // new Server("enode.duckdns.org", Server.ConnectionType.SSL, 50002), + // new Server("electrumx.ddns.net", Server.ConnectionType.SSL, 50002), + // new Server("electrumx.ml", Server.ConnectionType.SSL, 50002), + // new Server("electrum.eff.ro", Server.ConnectionType.SSL, 50002), + // new Server("electrum.bitkoins.nl", Server.ConnectionType.SSL, 50512), + // new Server("E-X.not.fyi", Server.ConnectionType.SSL, 50002), + // new Server("btc.electroncash.dk", Server.ConnectionType.SSL, 60002), + // new Server("electrum.blockstream.info", Server.ConnectionType.TCP, 50001), + // new Server("electrum.blockstream.info", Server.ConnectionType.SSL, 50002), + // new Server("bitcoin.aranguren.org", Server.ConnectionType.TCP, 50001), + )); break; case "TEST3": @@ -118,6 +134,7 @@ public class ElectrumX { rpc("server.banner"); } + /** Returns ElectrumX instance linked to passed Bitcoin network, one of "MAIN", "TEST3" or "REGTEST". */ public static synchronized ElectrumX getInstance(String bitcoinNetwork) { if (!instances.containsKey(bitcoinNetwork)) instances.put(bitcoinNetwork, new ElectrumX(bitcoinNetwork)); @@ -164,6 +181,7 @@ public class ElectrumX { return rawBlockHeaders; } + /** Returns confirmed balance, based on passed payment script, or null if there was an error or no known balance. */ public Long getBalance(byte[] script) { byte[] scriptHash = Crypto.digest(script); Bytes.reverse(scriptHash); @@ -180,6 +198,7 @@ public class ElectrumX { return (Long) balanceJson.get("confirmed"); } + /** Unspent output info as returned by ElectrumX network. */ public static class UnspentOutput { public final byte[] hash; public final int index; @@ -194,6 +213,7 @@ public class ElectrumX { } } + /** Returns list of unspent outputs pertaining to passed payment script, or null if there was an error. */ public List getUnspentOutputs(byte[] script) { byte[] scriptHash = Crypto.digest(script); Bytes.reverse(scriptHash); @@ -217,6 +237,7 @@ public class ElectrumX { return unspentOutputs; } + /** Returns raw transaction for passed transaction hash, or null if not found. */ public byte[] getRawTransaction(byte[] txHash) { Object rawTransactionHex = this.rpc("blockchain.transaction.get", HashCode.fromBytes(txHash).toString()); if (!(rawTransactionHex instanceof String)) @@ -225,7 +246,7 @@ public class ElectrumX { return HashCode.fromString((String) rawTransactionHex).asBytes(); } - /** Returns list of raw transactions. */ + /** Returns list of raw transactions, relating to passed payment script, if null if there's an error. */ public List getAddressTransactions(byte[] script) { byte[] scriptHash = Crypto.digest(script); Bytes.reverse(scriptHash); @@ -254,6 +275,7 @@ public class ElectrumX { return rawTransactions; } + /** Returns true if raw transaction successfully broadcast. */ public boolean broadcastTransaction(byte[] transactionBytes) { Object rawBroadcastResult = this.rpc("blockchain.transaction.broadcast", HashCode.fromBytes(transactionBytes).toString()); if (rawBroadcastResult == null) @@ -266,6 +288,7 @@ public class ElectrumX { // Class-private utility methods + /** Query current server for its list of peer servers, and return those we can parse. */ private Set serverPeersSubscribe() { Set newServers = new HashSet<>(); @@ -318,6 +341,7 @@ public class ElectrumX { return newServers; } + /** Return output from RPC call, with automatic reconnection to different server if needed. */ private synchronized Object rpc(String method, Object...params) { while (haveConnection()) { Object response = connectedRpc(method, params); @@ -336,6 +360,7 @@ public class ElectrumX { return null; } + /** Returns true if we have, or create, a connection to an ElectrumX server. */ private boolean haveConnection() { if (this.currentServer != null) return true;