From cc13d1d0f1493e416a1f2e9a0ab7a461627956e4 Mon Sep 17 00:00:00 2001 From: catbref Date: Mon, 8 Jun 2020 12:44:03 +0100 Subject: [PATCH] WIP commit --- .../api/resource/CrossChainResource.java | 35 ++++++++ .../java/org/qortal/controller/TradeBot.java | 87 ++++++++++++++++++- .../qortal/data/crosschain/TradeBotData.java | 41 +++++++-- .../repository/CrossChainRepository.java | 2 + .../hsqldb/HSQLDBCrossChainRepository.java | 39 +++++++-- .../hsqldb/HSQLDBDatabaseUpdates.java | 5 +- 6 files changed, 194 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/qortal/api/resource/CrossChainResource.java b/src/main/java/org/qortal/api/resource/CrossChainResource.java index f60deb23..b7cbfb1d 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainResource.java @@ -19,6 +19,7 @@ import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; +import javax.ws.rs.PathParam; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; @@ -43,6 +44,7 @@ import org.qortal.api.model.CrossChainBitcoinRefundRequest; import org.qortal.api.model.CrossChainBitcoinTemplateRequest; import org.qortal.api.model.CrossChainBuildRequest; import org.qortal.asset.Asset; +import org.qortal.controller.TradeBot; import org.qortal.crosschain.BTC; import org.qortal.crosschain.BTCACCT; import org.qortal.crypto.Crypto; @@ -717,6 +719,39 @@ public class CrossChainResource { } } + @POST + @Path("/tradebot/{ataddress}") + @Operation( + summary = "Respond to a trade offer", + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string")) + ) + } + ) + @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE}) + public String tradeBotResponder(@PathParam("ataddress") String atAddress) { + if (atAddress == null || !Crypto.isValidAtAddress(atAddress)) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + + // Extract data from cross-chain trading AT + try (final Repository repository = RepositoryManager.getRepository()) { + ATData atData = fetchAtDataWithChecking(repository, null, atAddress); // null to skip creator check + CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); + + if (crossChainTradeData.mode != Mode.OFFER) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + + String p2shAddress = TradeBot.startResponse(repository, crossChainTradeData); + if (p2shAddress == null) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_NETWORK_ISSUE); + + return p2shAddress; + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + private ATData fetchAtDataWithChecking(Repository repository, byte[] creatorPublicKey, String atAddress) throws DataException { ATData atData = repository.getATRepository().fromATAddress(atAddress); if (atData == null) diff --git a/src/main/java/org/qortal/controller/TradeBot.java b/src/main/java/org/qortal/controller/TradeBot.java index fe2b16d1..0a16bfbe 100644 --- a/src/main/java/org/qortal/controller/TradeBot.java +++ b/src/main/java/org/qortal/controller/TradeBot.java @@ -1,12 +1,34 @@ package org.qortal.controller; +import java.security.SecureRandom; +import java.util.List; +import java.util.Random; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.bitcoinj.core.Address; +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.LegacyAddress; +import org.bitcoinj.core.NetworkParameters; +import org.qortal.account.PrivateKeyAccount; +import org.qortal.api.ApiError; +import org.qortal.api.ApiExceptionFactory; +import org.qortal.crosschain.BTC; +import org.qortal.crosschain.BTCACCT; +import org.qortal.crypto.Crypto; +import org.qortal.data.at.ATData; +import org.qortal.data.crosschain.CrossChainTradeData; +import org.qortal.data.crosschain.TradeBotData; +import org.qortal.data.crosschain.CrossChainTradeData.Mode; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; public class TradeBot { private static final Logger LOGGER = LogManager.getLogger(TradeBot.class); - + private static final Random RANDOM = new SecureRandom(); + private static TradeBot instance; private TradeBot() { @@ -20,8 +42,71 @@ public class TradeBot { return instance; } + public static String startResponse(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException { + BTC btc = BTC.getInstance(); + NetworkParameters params = btc.getNetworkParameters(); + + byte[] tradePrivateKey = generateTradePrivateKey(); + byte[] secret = generateSecret(); + byte[] secretHash = Crypto.digest(secret); + + byte[] tradeNativePublicKey = deriveTradeNativePublicKey(tradePrivateKey); + byte[] tradeNativePublicKeyHash = Crypto.hash160(tradeNativePublicKey); + + byte[] tradeForeignPublicKey = deriveTradeForeignPublicKey(tradePrivateKey); + byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey); + + TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, TradeBotData.State.ALICE_WAITING_FOR_P2SH_A, + tradeNativePublicKey, tradeNativePublicKeyHash, secret, secretHash, + tradeForeignPublicKey, tradeForeignPublicKeyHash, crossChainTradeData.qortalAtAddress, null); + repository.getCrossChainRepository().save(tradeBotData); + + // P2SH_a to be funded + byte[] redeemScriptBytes = BTCACCT.buildScript(tradeForeignPublicKeyHash, crossChainTradeData.lockTime, crossChainTradeData.foreignPublicKeyHash, secretHash); + byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); + + Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); + return p2shAddress.toString(); + } + + private static byte[] generateTradePrivateKey() { + byte[] seed = new byte[32]; + RANDOM.nextBytes(seed); + return seed; + } + + private static byte[] deriveTradeNativePublicKey(byte[] privateKey) { + return PrivateKeyAccount.toPublicKey(privateKey); + } + + private static byte[] deriveTradeForeignPublicKey(byte[] privateKey) { + return ECKey.fromPrivate(privateKey).getPubKey(); + } + + private static byte[] generateSecret() { + byte[] secret = new byte[32]; + RANDOM.nextBytes(secret); + return secret; + } + public void onChainTipChange() { // Get repo for trade situations + try (final Repository repository = RepositoryManager.getRepository()) { + List allTradeBotData = repository.getCrossChainRepository().getAllTradeBotData(); + + for (TradeBotData tradeBotData : allTradeBotData) + switch (tradeBotData.getState()) { + case ALICE_START: + handleAliceStart(repository, tradeBotData); + break; + } + } catch (DataException e) { + LOGGER.error("Couldn't run trade bot due to repository issue", e); + } + } + + private void handleAliceStart(Repository repository, TradeBotData tradeBotData) { + } } diff --git a/src/main/java/org/qortal/data/crosschain/TradeBotData.java b/src/main/java/org/qortal/data/crosschain/TradeBotData.java index 25c27b83..12ed14e9 100644 --- a/src/main/java/org/qortal/data/crosschain/TradeBotData.java +++ b/src/main/java/org/qortal/data/crosschain/TradeBotData.java @@ -38,24 +38,37 @@ public class TradeBotData { @Schema(hidden = true) private byte[] tradePrivateKey; + private byte[] tradeNativePublicKey; + private byte[] tradeNativePublicKeyHash; + private byte[] secret; + private byte[] secretHash; + + private byte[] tradeForeignPublicKey; + private byte[] tradeForeignPublicKeyHash; private String atAddress; private byte[] lastTransactionSignature; - public TradeBotData(byte[] tradePrivateKey, State tradeState, byte[] secret, String atAddress, + public TradeBotData(byte[] tradePrivateKey, State tradeState, + byte[] tradeNativePublicKey, byte[] tradeNativePublicKeyHash, byte[] secret, byte[] secretHash, + byte[] tradeForeignPublicKey, byte[] tradeForeignPublicKeyHash, String atAddress, byte[] lastTransactionSignature) { this.tradePrivateKey = tradePrivateKey; this.tradeState = tradeState; + this.tradeNativePublicKey = tradeNativePublicKey; + this.tradeNativePublicKeyHash = tradeNativePublicKeyHash; this.secret = secret; + this.secretHash = secretHash; + this.tradeForeignPublicKey = tradeForeignPublicKey; + this.tradeForeignPublicKeyHash = tradeForeignPublicKeyHash; this.atAddress = atAddress; this.lastTransactionSignature = lastTransactionSignature; } - public TradeBotData(byte[] tradePrivateKey, State tradeState) { - this.tradePrivateKey = tradePrivateKey; - this.tradeState = tradeState; + public byte[] getTradePrivateKey() { + return this.tradePrivateKey; } public State getState() { @@ -66,16 +79,28 @@ public class TradeBotData { this.tradeState = state; } + public byte[] getTradeNativePublicKey() { + return this.tradeNativePublicKey; + } + + public byte[] getTradeNativePublicKeyHash() { + return this.tradeNativePublicKeyHash; + } + public byte[] getSecret() { return this.secret; } - public void setSecret(byte[] secret) { - this.secret = secret; + public byte[] getSecretHash() { + return this.secretHash; } - public byte[] getTradePrivateKey() { - return this.tradePrivateKey; + public byte[] getTradeForeignPublicKey() { + return this.tradeForeignPublicKey; + } + + public byte[] getTradeForeignPublicKeyHash() { + return this.tradeForeignPublicKeyHash; } public String getAtAddress() { diff --git a/src/main/java/org/qortal/repository/CrossChainRepository.java b/src/main/java/org/qortal/repository/CrossChainRepository.java index 2c0ef31b..e1b409a0 100644 --- a/src/main/java/org/qortal/repository/CrossChainRepository.java +++ b/src/main/java/org/qortal/repository/CrossChainRepository.java @@ -8,4 +8,6 @@ public interface CrossChainRepository { public List getAllTradeBotData() throws DataException; + public void save(TradeBotData tradeBotData) throws DataException; + } diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBCrossChainRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBCrossChainRepository.java index 07f29961..4d91dd6e 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBCrossChainRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBCrossChainRepository.java @@ -19,7 +19,8 @@ public class HSQLDBCrossChainRepository implements CrossChainRepository { @Override public List getAllTradeBotData() throws DataException { - String sql = "SELECT trade_private_key, trade_state, secret, at_address, last_transaction_signature " + String sql = "SELECT trade_private_key, trade_state, trade_native_public_key, trade_native_public_key_hash, " + + "secret, secret_hash, trade_foreign_public_key, trade_foreign_public_key_hash, at_address, last_transaction_signature " + "FROM TradeBotStates"; List allTradeBotData = new ArrayList<>(); @@ -35,11 +36,18 @@ public class HSQLDBCrossChainRepository implements CrossChainRepository { if (tradeState == null) throw new DataException("Illegal trade-bot trade-state fetched from repository"); - byte[] secret = resultSet.getBytes(3); - String atAddress = resultSet.getString(4); - byte[] lastTransactionSignature = resultSet.getBytes(5); + byte[] tradeNativePublicKey = resultSet.getBytes(3); + byte[] tradeNativePublicKeyHash = resultSet.getBytes(4); + byte[] secret = resultSet.getBytes(5); + byte[] secretHash = resultSet.getBytes(6); + byte[] tradeForeignPublicKey = resultSet.getBytes(7); + byte[] tradeForeignPublicKeyHash = resultSet.getBytes(8); + String atAddress = resultSet.getString(9); + byte[] lastTransactionSignature = resultSet.getBytes(10); - TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, tradeState, secret, atAddress, lastTransactionSignature); + TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, tradeState, + tradeNativePublicKey, tradeNativePublicKeyHash, secret, secretHash, + tradeForeignPublicKey, tradeForeignPublicKeyHash, atAddress, lastTransactionSignature); allTradeBotData.add(tradeBotData); } while (resultSet.next()); @@ -49,4 +57,25 @@ public class HSQLDBCrossChainRepository implements CrossChainRepository { } } + @Override + public void save(TradeBotData tradeBotData) throws DataException { + HSQLDBSaver saveHelper = new HSQLDBSaver("TradeBotStates"); + + saveHelper.bind("trade_private_key", tradeBotData.getTradePrivateKey()) + .bind("trade_state", tradeBotData.getState().value) + .bind("trade_native_public_key", tradeBotData.getTradeNativePublicKey()) + .bind("trade_native_public_key_hash", tradeBotData.getTradeNativePublicKeyHash()) + .bind("secret", tradeBotData.getSecret()).bind("secret_hash", tradeBotData.getSecretHash()) + .bind("trade_foreign_public_key", tradeBotData.getTradeForeignPublicKey()) + .bind("trade_foreign_public_key_hash", tradeBotData.getTradeForeignPublicKeyHash()) + .bind("at_address", tradeBotData.getAtAddress()) + .bind("last_transaction_signature", tradeBotData.getLastTransactionSignature()); + + try { + saveHelper.execute(this.repository); + } catch (SQLException e) { + throw new DataException("Unable to save trade bot data into repository", e); + } + } + } diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java index e142e956..4c70b825 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -621,7 +621,10 @@ public class HSQLDBDatabaseUpdates { case 20: // Trade bot stmt.execute("CREATE TABLE TradeBotStates (trade_private_key QortalPrivateKey NOT NULL, trade_state TINYINT NOT NULL, " - + "secret VARBINARY(32) NOT NULL, at_address QortalAddress, " + + "trade_native_public_key QortalPublicKey NOT NULL, trade_native_public_key_hash VARBINARY(32) NOT NULL, " + + "secret VARBINARY(32) NOT NULL, secret_hash VARBINARY(32) NOT NULL, " + + "trade_foreign_public_key QortalPublicKey NOT NULL, trade_foreign_public_key_hash VARBINARY(32) NOT NULL, " + + "at_address QortalAddress, " + "last_transaction_signature Signature, PRIMARY KEY (trade_private_key)"); break;