getEndStates() {
+ return this.endStates;
+ }
+
+ /**
+ * Creates a new trade-bot entry from the "Bob" viewpoint, i.e. OFFERing QORT in exchange for DGB.
+ *
+ * Generates:
+ *
+ * - new 'trade' private key
+ *
+ * Derives:
+ *
+ * - 'native' (as in Qortal) public key, public key hash, address (starting with Q)
+ * - 'foreign' (as in Digibyte) public key, public key hash
+ *
+ * A Qortal AT is then constructed including the following as constants in the 'data segment':
+ *
+ * - 'native'/Qortal 'trade' address - used as a MESSAGE contact
+ * - 'foreign'/Digibyte public key hash - used by Alice's P2SH scripts to allow redeem
+ * - QORT amount on offer by Bob
+ * - DGB amount expected in return by Bob (from Alice)
+ * - trading timeout, in case things go wrong and everyone needs to refund
+ *
+ * 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 byte[] createTrade(Repository repository, TradeBotCreateRequest tradeBotCreateRequest) throws DataException {
+ byte[] tradePrivateKey = TradeBot.generateTradePrivateKey();
+
+ byte[] tradeNativePublicKey = TradeBot.deriveTradeNativePublicKey(tradePrivateKey);
+ byte[] tradeNativePublicKeyHash = Crypto.hash160(tradeNativePublicKey);
+ String tradeNativeAddress = Crypto.toAddress(tradeNativePublicKey);
+
+ byte[] tradeForeignPublicKey = TradeBot.deriveTradeForeignPublicKey(tradePrivateKey);
+ byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
+
+ // Convert Digibyte receiving address into public key hash (we only support P2PKH at this time)
+ Address digibyteReceivingAddress;
+ try {
+ digibyteReceivingAddress = Address.fromString(Digibyte.getInstance().getNetworkParameters(), tradeBotCreateRequest.receivingAddress);
+ } catch (AddressFormatException e) {
+ throw new DataException("Unsupported Digibyte receiving address: " + tradeBotCreateRequest.receivingAddress);
+ }
+ if (digibyteReceivingAddress.getOutputScriptType() != ScriptType.P2PKH)
+ throw new DataException("Unsupported Digibyte receiving address: " + tradeBotCreateRequest.receivingAddress);
+
+ byte[] digibyteReceivingAccountInfo = digibyteReceivingAddress.getHash();
+
+ PublicKeyAccount creator = new PublicKeyAccount(repository, tradeBotCreateRequest.creatorPublicKey);
+
+ // Deploy AT
+ long timestamp = NTP.getTime();
+ byte[] reference = creator.getLastReference();
+ long fee = 0L;
+ byte[] signature = null;
+ BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, creator.getPublicKey(), fee, signature);
+
+ String name = "QORT/DGB ACCT";
+ String description = "QORT/DGB cross-chain trade";
+ String aTType = "ACCT";
+ String tags = "ACCT QORT DGB";
+ byte[] creationBytes = DigibyteACCTv3.buildQortalAT(tradeNativeAddress, tradeForeignPublicKeyHash, tradeBotCreateRequest.qortAmount,
+ tradeBotCreateRequest.foreignAmount, tradeBotCreateRequest.tradeTimeout);
+ long amount = tradeBotCreateRequest.fundingQortAmount;
+
+ DeployAtTransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, aTType, tags, creationBytes, amount, Asset.QORT);
+
+ DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
+ fee = deployAtTransaction.calcRecommendedFee();
+ deployAtTransactionData.setFee(fee);
+
+ DeployAtTransaction.ensureATAddress(deployAtTransactionData);
+ String atAddress = deployAtTransactionData.getAtAddress();
+
+ TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, DigibyteACCTv3.NAME,
+ State.BOB_WAITING_FOR_AT_CONFIRM.name(), State.BOB_WAITING_FOR_AT_CONFIRM.value,
+ creator.getAddress(), atAddress, timestamp, tradeBotCreateRequest.qortAmount,
+ tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
+ null, null,
+ SupportedBlockchain.DIGIBYTE.name(),
+ tradeForeignPublicKey, tradeForeignPublicKeyHash,
+ tradeBotCreateRequest.foreignAmount, null, null, null, digibyteReceivingAccountInfo);
+
+ TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Built AT %s. Waiting for deployment", atAddress));
+
+ // Attempt to backup the trade bot data
+ TradeBot.backupTradeBotData(repository, null);
+
+ // Return to user for signing and broadcast as we don't have their Qortal private key
+ try {
+ return DeployAtTransactionTransformer.toBytes(deployAtTransactionData);
+ } catch (TransformationException e) {
+ throw new DataException("Failed to transform DEPLOY_AT transaction?", e);
+ }
+ }
+
+ /**
+ * Creates a trade-bot entry from the 'Alice' viewpoint, i.e. matching DGB to an existing offer.
+ *
+ * Requires a chosen trade offer from Bob, passed by crossChainTradeData
+ * and access to a Digibyte 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 Digibyte 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 Digibyte main-net)
+ * or 'tprv' for (Digibyte 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 Digibyte amount expected by 'Bob'.
+ *
+ * If the Digibyte transaction is successfully broadcast to the network then
+ * we also send a MESSAGE to Bob's trade-bot to let them know.
+ *
+ * The trade-bot entry is saved to the repository and the cross-chain trading process commences.
+ *
+ * @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 Digibyte network, false otherwise
+ * @throws DataException
+ */
+ public ResponseResult startResponse(Repository repository, ATData atData, ACCT acct, CrossChainTradeData crossChainTradeData, String xprv58, String receivingAddress) throws DataException {
+ byte[] tradePrivateKey = TradeBot.generateTradePrivateKey();
+ byte[] secretA = TradeBot.generateSecret();
+ byte[] hashOfSecretA = Crypto.hash160(secretA);
+
+ byte[] tradeNativePublicKey = TradeBot.deriveTradeNativePublicKey(tradePrivateKey);
+ byte[] tradeNativePublicKeyHash = Crypto.hash160(tradeNativePublicKey);
+ String tradeNativeAddress = Crypto.toAddress(tradeNativePublicKey);
+
+ byte[] tradeForeignPublicKey = TradeBot.deriveTradeForeignPublicKey(tradePrivateKey);
+ byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
+ byte[] receivingPublicKeyHash = Base58.decode(receivingAddress); // Actually the whole address, not just PKH
+
+ // We need to generate lockTime-A: add tradeTimeout to now
+ long now = NTP.getTime();
+ int lockTimeA = crossChainTradeData.tradeTimeout * 60 + (int) (now / 1000L);
+
+ TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, DigibyteACCTv3.NAME,
+ State.ALICE_WAITING_FOR_AT_LOCK.name(), State.ALICE_WAITING_FOR_AT_LOCK.value,
+ receivingAddress, crossChainTradeData.qortalAtAddress, now, crossChainTradeData.qortAmount,
+ tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
+ secretA, hashOfSecretA,
+ SupportedBlockchain.DIGIBYTE.name(),
+ tradeForeignPublicKey, tradeForeignPublicKeyHash,
+ crossChainTradeData.expectedForeignAmount, xprv58, null, lockTimeA, receivingPublicKeyHash);
+
+ // Attempt to backup the trade bot data
+ // Include tradeBotData as an additional parameter, since it's not in the repository yet
+ TradeBot.backupTradeBotData(repository, Arrays.asList(tradeBotData));
+
+ // Check we have enough funds via xprv58 to fund P2SH to cover expectedForeignAmount
+ long p2shFee;
+ try {
+ p2shFee = Digibyte.getInstance().getP2shFee(now);
+ } catch (ForeignBlockchainException e) {
+ LOGGER.debug("Couldn't estimate Digibyte fees?");
+ return ResponseResult.NETWORK_ISSUE;
+ }
+
+ // Fee for redeem/refund is subtracted from P2SH-A balance.
+ // Do not include fee for funding transaction as this is covered by buildSpend()
+ long amountA = crossChainTradeData.expectedForeignAmount + p2shFee /*redeeming/refunding P2SH-A*/;
+
+ // P2SH-A to be funded
+ byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(tradeForeignPublicKeyHash, lockTimeA, crossChainTradeData.creatorForeignPKH, hashOfSecretA);
+ String p2shAddress = Digibyte.getInstance().deriveP2shAddress(redeemScriptBytes);
+
+ // Build transaction for funding P2SH-A
+ Transaction p2shFundingTransaction = Digibyte.getInstance().buildSpend(tradeBotData.getForeignKey(), p2shAddress, amountA);
+ if (p2shFundingTransaction == null) {
+ LOGGER.debug("Unable to build P2SH-A funding transaction - lack of funds?");
+ return ResponseResult.BALANCE_ISSUE;
+ }
+
+ try {
+ Digibyte.getInstance().broadcastTransaction(p2shFundingTransaction);
+ } catch (ForeignBlockchainException e) {
+ // We couldn't fund P2SH-A at this time
+ LOGGER.debug("Couldn't broadcast P2SH-A funding transaction?");
+ return ResponseResult.NETWORK_ISSUE;
+ }
+
+ // Attempt to send MESSAGE to Bob's Qortal trade address
+ byte[] messageData = DigibyteACCTv3.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA());
+ String messageRecipient = crossChainTradeData.qortalCreatorTradeAddress;
+
+ boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
+ if (!isMessageAlreadySent) {
+ PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
+ MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
+
+ messageTransaction.computeNonce();
+ messageTransaction.sign(sender);
+
+ // reset repository state to prevent deadlock
+ repository.discardChanges();
+ ValidationResult result = messageTransaction.importAsUnconfirmed();
+
+ if (result != ValidationResult.OK) {
+ LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
+ return ResponseResult.NETWORK_ISSUE;
+ }
+ }
+
+ TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress));
+
+ return ResponseResult.OK;
+ }
+
+ @Override
+ public boolean canDelete(Repository repository, TradeBotData tradeBotData) throws DataException {
+ State tradeBotState = State.valueOf(tradeBotData.getStateValue());
+ if (tradeBotState == null)
+ return true;
+
+ // If the AT doesn't exist then we might as well let the user tidy up
+ if (!repository.getATRepository().exists(tradeBotData.getAtAddress()))
+ return true;
+
+ switch (tradeBotState) {
+ case BOB_WAITING_FOR_AT_CONFIRM:
+ case ALICE_DONE:
+ case BOB_DONE:
+ case ALICE_REFUNDED:
+ case BOB_REFUNDED:
+ case ALICE_REFUNDING_A:
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ public void progress(Repository repository, TradeBotData tradeBotData) throws DataException, ForeignBlockchainException {
+ State tradeBotState = State.valueOf(tradeBotData.getStateValue());
+ if (tradeBotState == null) {
+ LOGGER.info(() -> String.format("Trade-bot entry for AT %s has invalid state?", tradeBotData.getAtAddress()));
+ return;
+ }
+
+ ATData atData = null;
+ CrossChainTradeData tradeData = null;
+
+ if (tradeBotState.requiresAtData) {
+ // Attempt to fetch AT data
+ atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress());
+ if (atData == null) {
+ LOGGER.debug(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress()));
+ return;
+ }
+
+ if (tradeBotState.requiresTradeData) {
+ tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
+ if (tradeData == null) {
+ LOGGER.warn(() -> String.format("Unable to fetch ACCT trade data for AT %s from repository", tradeBotData.getAtAddress()));
+ return;
+ }
+ }
+ }
+
+ switch (tradeBotState) {
+ case BOB_WAITING_FOR_AT_CONFIRM:
+ handleBobWaitingForAtConfirm(repository, tradeBotData);
+ break;
+
+ case BOB_WAITING_FOR_MESSAGE:
+ TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
+ handleBobWaitingForMessage(repository, tradeBotData, atData, tradeData);
+ break;
+
+ case ALICE_WAITING_FOR_AT_LOCK:
+ TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
+ handleAliceWaitingForAtLock(repository, tradeBotData, atData, tradeData);
+ break;
+
+ case BOB_WAITING_FOR_AT_REDEEM:
+ TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
+ handleBobWaitingForAtRedeem(repository, tradeBotData, atData, tradeData);
+ break;
+
+ case ALICE_DONE:
+ case BOB_DONE:
+ break;
+
+ case ALICE_REFUNDING_A:
+ TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
+ handleAliceRefundingP2shA(repository, tradeBotData, atData, tradeData);
+ break;
+
+ case ALICE_REFUNDED:
+ case BOB_REFUNDED:
+ break;
+ }
+ }
+
+ /**
+ * 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())) {
+ if (NTP.getTime() - tradeBotData.getTimestamp() <= MAX_AT_CONFIRMATION_PERIOD)
+ return;
+
+ // We've waited ages for AT to be confirmed into a block but something has gone awry.
+ // After this long we assume transaction loss so give up with trade-bot entry too.
+ tradeBotData.setState(State.BOB_REFUNDED.name());
+ tradeBotData.setStateValue(State.BOB_REFUNDED.value);
+ tradeBotData.setTimestamp(NTP.getTime());
+ // We delete trade-bot entry here instead of saving, hence not using updateTradeBotState()
+ repository.getCrossChainRepository().delete(tradeBotData.getTradePrivateKey());
+ repository.saveChanges();
+
+ LOGGER.info(() -> String.format("AT %s never confirmed. Giving up on trade", tradeBotData.getAtAddress()));
+ TradeBot.notifyStateChange(tradeBotData);
+ return;
+ }
+
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_WAITING_FOR_MESSAGE,
+ () -> String.format("AT %s confirmed ready. Waiting for trade message", 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 Digibyte 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 Alice to redeem the AT, which will allow Bob to
+ * extract secret-A needed to redeem Alice's P2SH.
+ * @throws ForeignBlockchainException
+ */
+ private void handleBobWaitingForMessage(Repository repository, TradeBotData tradeBotData,
+ ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
+ // If AT has finished then Bob likely cancelled his trade offer
+ if (atData.getIsFinished()) {
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED,
+ () -> String.format("AT %s cancelled - trading aborted", tradeBotData.getAtAddress()));
+ return;
+ }
+
+ Digibyte digibyte = Digibyte.getInstance();
+
+ String address = tradeBotData.getTradeNativeAddress();
+ List messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, address, null, null, null);
+
+ for (MessageTransactionData messageTransactionData : messageTransactionsData) {
+ if (messageTransactionData.isText())
+ continue;
+
+ // We're expecting: HASH160(secret-A), Alice's Digibyte pubkeyhash and lockTime-A
+ byte[] messageData = messageTransactionData.getData();
+ DigibyteACCTv3.OfferMessageData offerMessageData = DigibyteACCTv3.extractOfferMessageData(messageData);
+ if (offerMessageData == null)
+ continue;
+
+ byte[] aliceForeignPublicKeyHash = offerMessageData.partnerDigibytePKH;
+ byte[] hashOfSecretA = offerMessageData.hashOfSecretA;
+ int lockTimeA = (int) offerMessageData.lockTimeA;
+ long messageTimestamp = messageTransactionData.getTimestamp();
+ int refundTimeout = DigibyteACCTv3.calcRefundTimeout(messageTimestamp, lockTimeA);
+
+ // Determine P2SH-A address and confirm funded
+ byte[] redeemScriptA = BitcoinyHTLC.buildScript(aliceForeignPublicKeyHash, lockTimeA, tradeBotData.getTradeForeignPublicKeyHash(), hashOfSecretA);
+ String p2shAddressA = digibyte.deriveP2shAddress(redeemScriptA);
+
+ long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
+ long p2shFee = Digibyte.getInstance().getP2shFee(feeTimestamp);
+ final long minimumAmountA = tradeBotData.getForeignAmount() + p2shFee;
+
+ BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(digibyte.getBlockchainProvider(), p2shAddressA, minimumAmountA);
+
+ switch (htlcStatusA) {
+ case UNFUNDED:
+ case FUNDING_IN_PROGRESS:
+ // There might be another MESSAGE from someone else with an actually funded P2SH-A...
+ continue;
+
+ case REDEEM_IN_PROGRESS:
+ 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));
+ return;
+
+ case REFUND_IN_PROGRESS:
+ case REFUNDED:
+ // This P2SH-A is burnt, but there might be another MESSAGE from someone else with an actually funded P2SH-A...
+ continue;
+
+ case FUNDED:
+ // Fall-through out of switch...
+ break;
+ }
+
+ // Good to go - send MESSAGE to AT
+
+ String aliceNativeAddress = Crypto.toAddress(messageTransactionData.getCreatorPublicKey());
+
+ // Build outgoing message, padding each part to 32 bytes to make it easier for AT to consume
+ byte[] outgoingMessageData = DigibyteACCTv3.buildTradeMessage(aliceNativeAddress, aliceForeignPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
+ String messageRecipient = tradeBotData.getAtAddress();
+
+ boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, outgoingMessageData);
+ if (!isMessageAlreadySent) {
+ PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
+ MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false);
+
+ outgoingMessageTransaction.computeNonce();
+ outgoingMessageTransaction.sign(sender);
+
+ // reset repository state to prevent deadlock
+ repository.discardChanges();
+ ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
+
+ if (result != ValidationResult.OK) {
+ LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
+ return;
+ }
+ }
+
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_WAITING_FOR_AT_REDEEM,
+ () -> String.format("Locked AT %s to %s. Waiting for AT redeem", tradeBotData.getAtAddress(), aliceNativeAddress));
+
+ return;
+ }
+ }
+
+ /**
+ * 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 redeems AT using Alice's secret-A, releasing Bob's QORT to Alice.
+ *
+ * In revealing a valid secret-A, Bob can then redeem the DGB funds from P2SH-A.
+ *
+ * @throws ForeignBlockchainException
+ */
+ private void handleAliceWaitingForAtLock(Repository repository, TradeBotData tradeBotData,
+ ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
+ if (aliceUnexpectedState(repository, tradeBotData, atData, crossChainTradeData))
+ return;
+
+ Digibyte digibyte = Digibyte.getInstance();
+ int lockTimeA = tradeBotData.getLockTimeA();
+
+ // Refund P2SH-A if we've passed lockTime-A
+ if (NTP.getTime() >= lockTimeA * 1000L) {
+ byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
+ String p2shAddressA = digibyte.deriveP2shAddress(redeemScriptA);
+
+ long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
+ long p2shFee = Digibyte.getInstance().getP2shFee(feeTimestamp);
+ long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
+
+ BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(digibyte.getBlockchainProvider(), p2shAddressA, minimumAmountA);
+
+ switch (htlcStatusA) {
+ case UNFUNDED:
+ case FUNDING_IN_PROGRESS:
+ case FUNDED:
+ break;
+
+ case REDEEM_IN_PROGRESS:
+ case REDEEMED:
+ // Already redeemed?
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
+ () -> String.format("P2SH-A %s already spent? Assuming trade completed", p2shAddressA));
+ return;
+
+ case REFUND_IN_PROGRESS:
+ case REFUNDED:
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED,
+ () -> String.format("P2SH-A %s already refunded. Trade aborted", p2shAddressA));
+ 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));
+
+ return;
+ }
+
+ // We're waiting for AT to be in TRADE mode
+ if (crossChainTradeData.mode != AcctMode.TRADING)
+ return;
+
+ // AT is in TRADE mode and locked to us as checked by aliceUnexpectedState() above
+
+ // Find our MESSAGE to AT from previous state
+ List messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(tradeBotData.getTradeNativePublicKey(),
+ crossChainTradeData.qortalCreatorTradeAddress, null, null, null);
+ if (messageTransactionsData == null || messageTransactionsData.isEmpty()) {
+ LOGGER.warn(() -> String.format("Unable to find our message to trade creator %s?", crossChainTradeData.qortalCreatorTradeAddress));
+ return;
+ }
+
+ long recipientMessageTimestamp = messageTransactionsData.get(0).getTimestamp();
+ int refundTimeout = DigibyteACCTv3.calcRefundTimeout(recipientMessageTimestamp, lockTimeA);
+
+ // Our calculated refundTimeout should match AT's refundTimeout
+ if (refundTimeout != crossChainTradeData.refundTimeout) {
+ LOGGER.debug(() -> String.format("Trade AT refundTimeout '%d' doesn't match our refundTimeout '%d'", crossChainTradeData.refundTimeout, refundTimeout));
+ // We'll eventually refund
+ return;
+ }
+
+ // We're good to redeem AT
+
+ // Send 'redeem' MESSAGE to AT using both secret
+ byte[] secretA = tradeBotData.getSecret();
+ String qortalReceivingAddress = Base58.encode(tradeBotData.getReceivingAccountInfo()); // Actually contains whole address, not just PKH
+ byte[] messageData = DigibyteACCTv3.buildRedeemMessage(secretA, qortalReceivingAddress);
+ String messageRecipient = tradeBotData.getAtAddress();
+
+ boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
+ if (!isMessageAlreadySent) {
+ PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
+ MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
+
+ messageTransaction.computeNonce();
+ messageTransaction.sign(sender);
+
+ // Reset repository state to prevent deadlock
+ repository.discardChanges();
+ ValidationResult result = messageTransaction.importAsUnconfirmed();
+
+ if (result != ValidationResult.OK) {
+ LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
+ return;
+ }
+ }
+
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
+ () -> String.format("Redeeming AT %s. Funds should arrive at %s",
+ tradeBotData.getAtAddress(), qortalReceivingAddress));
+ }
+
+ /**
+ * Trade-bot is waiting for Alice to redeem Bob's AT, thus revealing secret-A which is required to spend the DGB 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 DGB funds from P2SH-A
+ * to Bob's 'foreign'/Digibyte trade legacy-format address, as derived from trade private key.
+ *
+ * (This could potentially be 'improved' to send DGB 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.
+ * @throws ForeignBlockchainException
+ */
+ private void handleBobWaitingForAtRedeem(Repository repository, TradeBotData tradeBotData,
+ ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
+ // AT should be 'finished' once Alice has redeemed QORT funds
+ if (!atData.getIsFinished())
+ // Not finished yet
+ return;
+
+ // If AT is REFUNDED or CANCELLED then something has gone wrong
+ if (crossChainTradeData.mode == AcctMode.REFUNDED || crossChainTradeData.mode == AcctMode.CANCELLED) {
+ // Alice hasn't redeemed the QORT, so there is no point in trying to redeem the DGB
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED,
+ () -> String.format("AT %s has auto-refunded - trade aborted", tradeBotData.getAtAddress()));
+
+ return;
+ }
+
+ byte[] secretA = DigibyteACCTv3.getInstance().findSecretA(repository, crossChainTradeData);
+ if (secretA == null) {
+ LOGGER.debug(() -> String.format("Unable to find secret-A from redeem message to AT %s?", tradeBotData.getAtAddress()));
+ return;
+ }
+
+ // Use secret-A to redeem P2SH-A
+
+ Digibyte digibyte = Digibyte.getInstance();
+
+ byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo();
+ int lockTimeA = crossChainTradeData.lockTimeA;
+ byte[] redeemScriptA = BitcoinyHTLC.buildScript(crossChainTradeData.partnerForeignPKH, lockTimeA, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretA);
+ String p2shAddressA = digibyte.deriveP2shAddress(redeemScriptA);
+
+ // Fee for redeem/refund is subtracted from P2SH-A balance.
+ long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
+ long p2shFee = Digibyte.getInstance().getP2shFee(feeTimestamp);
+ long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
+ BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(digibyte.getBlockchainProvider(), p2shAddressA, minimumAmountA);
+
+ switch (htlcStatusA) {
+ case UNFUNDED:
+ case FUNDING_IN_PROGRESS:
+ // P2SH-A suddenly not funded? Our best bet at this point is to hope for AT auto-refund
+ return;
+
+ case REDEEM_IN_PROGRESS:
+ case REDEEMED:
+ // Double-check that we have redeemed P2SH-A...
+ break;
+
+ case REFUND_IN_PROGRESS:
+ case REFUNDED:
+ // Wait for AT to auto-refund
+ return;
+
+ case FUNDED: {
+ Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
+ ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
+ List fundingOutputs = digibyte.getUnspentOutputs(p2shAddressA);
+
+ Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(digibyte.getNetworkParameters(), redeemAmount, redeemKey,
+ fundingOutputs, redeemScriptA, secretA, receivingAccountInfo);
+
+ digibyte.broadcastTransaction(p2shRedeemTransaction);
+ break;
+ }
+ }
+
+ String receivingAddress = digibyte.pkhToAddress(receivingAccountInfo);
+
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_DONE,
+ () -> String.format("P2SH-A %s redeemed. Funds should arrive at %s", tradeBotData.getAtAddress(), receivingAddress));
+ }
+
+ /**
+ * Trade-bot is attempting to refund P2SH-A.
+ * @throws ForeignBlockchainException
+ */
+ private void handleAliceRefundingP2shA(Repository repository, TradeBotData tradeBotData,
+ ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
+ int lockTimeA = tradeBotData.getLockTimeA();
+
+ // We can't refund P2SH-A until lockTime-A has passed
+ if (NTP.getTime() <= lockTimeA * 1000L)
+ return;
+
+ Digibyte digibyte = Digibyte.getInstance();
+
+ // We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
+ int medianBlockTime = digibyte.getMedianBlockTime();
+ if (medianBlockTime <= lockTimeA)
+ return;
+
+ byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
+ String p2shAddressA = digibyte.deriveP2shAddress(redeemScriptA);
+
+ // Fee for redeem/refund is subtracted from P2SH-A balance.
+ long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
+ long p2shFee = Digibyte.getInstance().getP2shFee(feeTimestamp);
+ long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
+ BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(digibyte.getBlockchainProvider(), p2shAddressA, minimumAmountA);
+
+ switch (htlcStatusA) {
+ case UNFUNDED:
+ case FUNDING_IN_PROGRESS:
+ // Still waiting for P2SH-A to be funded...
+ return;
+
+ case REDEEM_IN_PROGRESS:
+ case REDEEMED:
+ // Too late!
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
+ () -> String.format("P2SH-A %s already spent!", p2shAddressA));
+ return;
+
+ case REFUND_IN_PROGRESS:
+ case REFUNDED:
+ break;
+
+ case FUNDED:{
+ Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
+ ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
+ List fundingOutputs = digibyte.getUnspentOutputs(p2shAddressA);
+
+ // Determine receive address for refund
+ String receiveAddress = digibyte.getUnusedReceiveAddress(tradeBotData.getForeignKey());
+ Address receiving = Address.fromString(digibyte.getNetworkParameters(), receiveAddress);
+
+ Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(digibyte.getNetworkParameters(), refundAmount, refundKey,
+ fundingOutputs, redeemScriptA, lockTimeA, receiving.getHash());
+
+ digibyte.broadcastTransaction(p2shRefundTransaction);
+ break;
+ }
+ }
+
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED,
+ () -> String.format("LockTime-A reached. Refunded P2SH-A %s. Trade aborted", p2shAddressA));
+ }
+
+ /**
+ * Returns true if Alice finds AT unexpectedly cancelled, refunded, redeemed or locked to someone else.
+ *
+ * Will automatically update trade-bot state to ALICE_REFUNDING_A or ALICE_DONE as necessary.
+ *
+ * @throws DataException
+ * @throws ForeignBlockchainException
+ */
+ private boolean aliceUnexpectedState(Repository repository, TradeBotData tradeBotData,
+ ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
+ // This is OK
+ if (!atData.getIsFinished() && crossChainTradeData.mode == AcctMode.OFFERING)
+ return false;
+
+ boolean isAtLockedToUs = tradeBotData.getTradeNativeAddress().equals(crossChainTradeData.qortalPartnerAddress);
+
+ if (!atData.getIsFinished() && crossChainTradeData.mode == AcctMode.TRADING)
+ if (isAtLockedToUs) {
+ // AT is trading with us - OK
+ return false;
+ } else {
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
+ () -> String.format("AT %s trading with someone else: %s. Refunding & aborting trade", tradeBotData.getAtAddress(), crossChainTradeData.qortalPartnerAddress));
+
+ return true;
+ }
+
+ if (atData.getIsFinished() && crossChainTradeData.mode == AcctMode.REDEEMED && isAtLockedToUs) {
+ // We've redeemed already?
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
+ () -> String.format("AT %s already redeemed by us. Trade completed", tradeBotData.getAtAddress()));
+ } else {
+ // Any other state is not good, so start defensive refund
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
+ () -> String.format("AT %s cancelled/refunded/redeemed by someone else/invalid state. Refunding & aborting trade", tradeBotData.getAtAddress()));
+ }
+
+ return true;
+ }
+
+ private long calcFeeTimestamp(int lockTimeA, int tradeTimeout) {
+ return (lockTimeA - tradeTimeout * 60) * 1000L;
+ }
+
+}
diff --git a/src/main/java/org/qortal/controller/tradebot/RavencoinACCTv3TradeBot.java b/src/main/java/org/qortal/controller/tradebot/RavencoinACCTv3TradeBot.java
new file mode 100644
index 00000000..80fe7932
--- /dev/null
+++ b/src/main/java/org/qortal/controller/tradebot/RavencoinACCTv3TradeBot.java
@@ -0,0 +1,885 @@
+package org.qortal.controller.tradebot;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.bitcoinj.core.*;
+import org.bitcoinj.script.Script.ScriptType;
+import org.qortal.account.PrivateKeyAccount;
+import org.qortal.account.PublicKeyAccount;
+import org.qortal.api.model.crosschain.TradeBotCreateRequest;
+import org.qortal.asset.Asset;
+import org.qortal.crosschain.*;
+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.transaction.BaseTransactionData;
+import org.qortal.data.transaction.DeployAtTransactionData;
+import org.qortal.data.transaction.MessageTransactionData;
+import org.qortal.group.Group;
+import org.qortal.repository.DataException;
+import org.qortal.repository.Repository;
+import org.qortal.transaction.DeployAtTransaction;
+import org.qortal.transaction.MessageTransaction;
+import org.qortal.transaction.Transaction.ValidationResult;
+import org.qortal.transform.TransformationException;
+import org.qortal.transform.transaction.DeployAtTransactionTransformer;
+import org.qortal.utils.Base58;
+import org.qortal.utils.NTP;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static java.util.Arrays.stream;
+import static java.util.stream.Collectors.toMap;
+
+/**
+ * Performing cross-chain trading steps on behalf of user.
+ *
+ * We deal with three different independent state-spaces here:
+ *
+ * - Qortal blockchain
+ * - Foreign blockchain
+ * - Trade-bot entries
+ *
+ */
+public class RavencoinACCTv3TradeBot implements AcctTradeBot {
+
+ private static final Logger LOGGER = LogManager.getLogger(RavencoinACCTv3TradeBot.class);
+
+ public enum State implements TradeBot.StateNameAndValueSupplier {
+ BOB_WAITING_FOR_AT_CONFIRM(10, false, false),
+ BOB_WAITING_FOR_MESSAGE(15, true, true),
+ BOB_WAITING_FOR_AT_REDEEM(25, true, true),
+ BOB_DONE(30, false, false),
+ BOB_REFUNDED(35, false, false),
+
+ ALICE_WAITING_FOR_AT_LOCK(85, true, true),
+ ALICE_DONE(95, false, false),
+ ALICE_REFUNDING_A(105, true, true),
+ ALICE_REFUNDED(110, false, false);
+
+ private static final Map map = stream(State.values()).collect(toMap(state -> state.value, state -> state));
+
+ public final int value;
+ public final boolean requiresAtData;
+ public final boolean requiresTradeData;
+
+ State(int value, boolean requiresAtData, boolean requiresTradeData) {
+ this.value = value;
+ this.requiresAtData = requiresAtData;
+ this.requiresTradeData = requiresTradeData;
+ }
+
+ public static State valueOf(int value) {
+ return map.get(value);
+ }
+
+ @Override
+ public String getState() {
+ return this.name();
+ }
+
+ @Override
+ public int getStateValue() {
+ return this.value;
+ }
+ }
+
+ /** Maximum time Bob waits for his AT creation transaction to be confirmed into a block. (milliseconds) */
+ private static final long MAX_AT_CONFIRMATION_PERIOD = 24 * 60 * 60 * 1000L; // ms
+
+ private static RavencoinACCTv3TradeBot instance;
+
+ private final List endStates = Arrays.asList(State.BOB_DONE, State.BOB_REFUNDED, State.ALICE_DONE, State.ALICE_REFUNDING_A, State.ALICE_REFUNDED).stream()
+ .map(State::name)
+ .collect(Collectors.toUnmodifiableList());
+
+ private RavencoinACCTv3TradeBot() {
+ }
+
+ public static synchronized RavencoinACCTv3TradeBot getInstance() {
+ if (instance == null)
+ instance = new RavencoinACCTv3TradeBot();
+
+ return instance;
+ }
+
+ @Override
+ public List getEndStates() {
+ return this.endStates;
+ }
+
+ /**
+ * Creates a new trade-bot entry from the "Bob" viewpoint, i.e. OFFERing QORT in exchange for RVN.
+ *
+ * Generates:
+ *
+ * - new 'trade' private key
+ *
+ * Derives:
+ *
+ * - 'native' (as in Qortal) public key, public key hash, address (starting with Q)
+ * - 'foreign' (as in Ravencoin) public key, public key hash
+ *
+ * A Qortal AT is then constructed including the following as constants in the 'data segment':
+ *
+ * - 'native'/Qortal 'trade' address - used as a MESSAGE contact
+ * - 'foreign'/Ravencoin public key hash - used by Alice's P2SH scripts to allow redeem
+ * - QORT amount on offer by Bob
+ * - RVN amount expected in return by Bob (from Alice)
+ * - trading timeout, in case things go wrong and everyone needs to refund
+ *
+ * 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 byte[] createTrade(Repository repository, TradeBotCreateRequest tradeBotCreateRequest) throws DataException {
+ byte[] tradePrivateKey = TradeBot.generateTradePrivateKey();
+
+ byte[] tradeNativePublicKey = TradeBot.deriveTradeNativePublicKey(tradePrivateKey);
+ byte[] tradeNativePublicKeyHash = Crypto.hash160(tradeNativePublicKey);
+ String tradeNativeAddress = Crypto.toAddress(tradeNativePublicKey);
+
+ byte[] tradeForeignPublicKey = TradeBot.deriveTradeForeignPublicKey(tradePrivateKey);
+ byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
+
+ // Convert Ravencoin receiving address into public key hash (we only support P2PKH at this time)
+ Address ravencoinReceivingAddress;
+ try {
+ ravencoinReceivingAddress = Address.fromString(Ravencoin.getInstance().getNetworkParameters(), tradeBotCreateRequest.receivingAddress);
+ } catch (AddressFormatException e) {
+ throw new DataException("Unsupported Ravencoin receiving address: " + tradeBotCreateRequest.receivingAddress);
+ }
+ if (ravencoinReceivingAddress.getOutputScriptType() != ScriptType.P2PKH)
+ throw new DataException("Unsupported Ravencoin receiving address: " + tradeBotCreateRequest.receivingAddress);
+
+ byte[] ravencoinReceivingAccountInfo = ravencoinReceivingAddress.getHash();
+
+ PublicKeyAccount creator = new PublicKeyAccount(repository, tradeBotCreateRequest.creatorPublicKey);
+
+ // Deploy AT
+ long timestamp = NTP.getTime();
+ byte[] reference = creator.getLastReference();
+ long fee = 0L;
+ byte[] signature = null;
+ BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, creator.getPublicKey(), fee, signature);
+
+ String name = "QORT/RVN ACCT";
+ String description = "QORT/RVN cross-chain trade";
+ String aTType = "ACCT";
+ String tags = "ACCT QORT RVN";
+ byte[] creationBytes = RavencoinACCTv3.buildQortalAT(tradeNativeAddress, tradeForeignPublicKeyHash, tradeBotCreateRequest.qortAmount,
+ tradeBotCreateRequest.foreignAmount, tradeBotCreateRequest.tradeTimeout);
+ long amount = tradeBotCreateRequest.fundingQortAmount;
+
+ DeployAtTransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, aTType, tags, creationBytes, amount, Asset.QORT);
+
+ DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
+ fee = deployAtTransaction.calcRecommendedFee();
+ deployAtTransactionData.setFee(fee);
+
+ DeployAtTransaction.ensureATAddress(deployAtTransactionData);
+ String atAddress = deployAtTransactionData.getAtAddress();
+
+ TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, RavencoinACCTv3.NAME,
+ State.BOB_WAITING_FOR_AT_CONFIRM.name(), State.BOB_WAITING_FOR_AT_CONFIRM.value,
+ creator.getAddress(), atAddress, timestamp, tradeBotCreateRequest.qortAmount,
+ tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
+ null, null,
+ SupportedBlockchain.RAVENCOIN.name(),
+ tradeForeignPublicKey, tradeForeignPublicKeyHash,
+ tradeBotCreateRequest.foreignAmount, null, null, null, ravencoinReceivingAccountInfo);
+
+ TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Built AT %s. Waiting for deployment", atAddress));
+
+ // Attempt to backup the trade bot data
+ TradeBot.backupTradeBotData(repository, null);
+
+ // Return to user for signing and broadcast as we don't have their Qortal private key
+ try {
+ return DeployAtTransactionTransformer.toBytes(deployAtTransactionData);
+ } catch (TransformationException e) {
+ throw new DataException("Failed to transform DEPLOY_AT transaction?", e);
+ }
+ }
+
+ /**
+ * Creates a trade-bot entry from the 'Alice' viewpoint, i.e. matching RVN to an existing offer.
+ *
+ * Requires a chosen trade offer from Bob, passed by crossChainTradeData
+ * and access to a Ravencoin 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 Ravencoin 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 Ravencoin main-net)
+ * or 'tprv' for (Ravencoin 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 Ravencoin amount expected by 'Bob'.
+ *
+ * If the Ravencoin transaction is successfully broadcast to the network then
+ * we also send a MESSAGE to Bob's trade-bot to let them know.
+ *
+ * The trade-bot entry is saved to the repository and the cross-chain trading process commences.
+ *
+ * @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 Ravencoin network, false otherwise
+ * @throws DataException
+ */
+ public ResponseResult startResponse(Repository repository, ATData atData, ACCT acct, CrossChainTradeData crossChainTradeData, String xprv58, String receivingAddress) throws DataException {
+ byte[] tradePrivateKey = TradeBot.generateTradePrivateKey();
+ byte[] secretA = TradeBot.generateSecret();
+ byte[] hashOfSecretA = Crypto.hash160(secretA);
+
+ byte[] tradeNativePublicKey = TradeBot.deriveTradeNativePublicKey(tradePrivateKey);
+ byte[] tradeNativePublicKeyHash = Crypto.hash160(tradeNativePublicKey);
+ String tradeNativeAddress = Crypto.toAddress(tradeNativePublicKey);
+
+ byte[] tradeForeignPublicKey = TradeBot.deriveTradeForeignPublicKey(tradePrivateKey);
+ byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
+ byte[] receivingPublicKeyHash = Base58.decode(receivingAddress); // Actually the whole address, not just PKH
+
+ // We need to generate lockTime-A: add tradeTimeout to now
+ long now = NTP.getTime();
+ int lockTimeA = crossChainTradeData.tradeTimeout * 60 + (int) (now / 1000L);
+
+ TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, RavencoinACCTv3.NAME,
+ State.ALICE_WAITING_FOR_AT_LOCK.name(), State.ALICE_WAITING_FOR_AT_LOCK.value,
+ receivingAddress, crossChainTradeData.qortalAtAddress, now, crossChainTradeData.qortAmount,
+ tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
+ secretA, hashOfSecretA,
+ SupportedBlockchain.RAVENCOIN.name(),
+ tradeForeignPublicKey, tradeForeignPublicKeyHash,
+ crossChainTradeData.expectedForeignAmount, xprv58, null, lockTimeA, receivingPublicKeyHash);
+
+ // Attempt to backup the trade bot data
+ // Include tradeBotData as an additional parameter, since it's not in the repository yet
+ TradeBot.backupTradeBotData(repository, Arrays.asList(tradeBotData));
+
+ // Check we have enough funds via xprv58 to fund P2SH to cover expectedForeignAmount
+ long p2shFee;
+ try {
+ p2shFee = Ravencoin.getInstance().getP2shFee(now);
+ } catch (ForeignBlockchainException e) {
+ LOGGER.debug("Couldn't estimate Ravencoin fees?");
+ return ResponseResult.NETWORK_ISSUE;
+ }
+
+ // Fee for redeem/refund is subtracted from P2SH-A balance.
+ // Do not include fee for funding transaction as this is covered by buildSpend()
+ long amountA = crossChainTradeData.expectedForeignAmount + p2shFee /*redeeming/refunding P2SH-A*/;
+
+ // P2SH-A to be funded
+ byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(tradeForeignPublicKeyHash, lockTimeA, crossChainTradeData.creatorForeignPKH, hashOfSecretA);
+ String p2shAddress = Ravencoin.getInstance().deriveP2shAddress(redeemScriptBytes);
+
+ // Build transaction for funding P2SH-A
+ Transaction p2shFundingTransaction = Ravencoin.getInstance().buildSpend(tradeBotData.getForeignKey(), p2shAddress, amountA);
+ if (p2shFundingTransaction == null) {
+ LOGGER.debug("Unable to build P2SH-A funding transaction - lack of funds?");
+ return ResponseResult.BALANCE_ISSUE;
+ }
+
+ try {
+ Ravencoin.getInstance().broadcastTransaction(p2shFundingTransaction);
+ } catch (ForeignBlockchainException e) {
+ // We couldn't fund P2SH-A at this time
+ LOGGER.debug("Couldn't broadcast P2SH-A funding transaction?");
+ return ResponseResult.NETWORK_ISSUE;
+ }
+
+ // Attempt to send MESSAGE to Bob's Qortal trade address
+ byte[] messageData = RavencoinACCTv3.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA());
+ String messageRecipient = crossChainTradeData.qortalCreatorTradeAddress;
+
+ boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
+ if (!isMessageAlreadySent) {
+ PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
+ MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
+
+ messageTransaction.computeNonce();
+ messageTransaction.sign(sender);
+
+ // reset repository state to prevent deadlock
+ repository.discardChanges();
+ ValidationResult result = messageTransaction.importAsUnconfirmed();
+
+ if (result != ValidationResult.OK) {
+ LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
+ return ResponseResult.NETWORK_ISSUE;
+ }
+ }
+
+ TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress));
+
+ return ResponseResult.OK;
+ }
+
+ @Override
+ public boolean canDelete(Repository repository, TradeBotData tradeBotData) throws DataException {
+ State tradeBotState = State.valueOf(tradeBotData.getStateValue());
+ if (tradeBotState == null)
+ return true;
+
+ // If the AT doesn't exist then we might as well let the user tidy up
+ if (!repository.getATRepository().exists(tradeBotData.getAtAddress()))
+ return true;
+
+ switch (tradeBotState) {
+ case BOB_WAITING_FOR_AT_CONFIRM:
+ case ALICE_DONE:
+ case BOB_DONE:
+ case ALICE_REFUNDED:
+ case BOB_REFUNDED:
+ case ALICE_REFUNDING_A:
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ public void progress(Repository repository, TradeBotData tradeBotData) throws DataException, ForeignBlockchainException {
+ State tradeBotState = State.valueOf(tradeBotData.getStateValue());
+ if (tradeBotState == null) {
+ LOGGER.info(() -> String.format("Trade-bot entry for AT %s has invalid state?", tradeBotData.getAtAddress()));
+ return;
+ }
+
+ ATData atData = null;
+ CrossChainTradeData tradeData = null;
+
+ if (tradeBotState.requiresAtData) {
+ // Attempt to fetch AT data
+ atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress());
+ if (atData == null) {
+ LOGGER.debug(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress()));
+ return;
+ }
+
+ if (tradeBotState.requiresTradeData) {
+ tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
+ if (tradeData == null) {
+ LOGGER.warn(() -> String.format("Unable to fetch ACCT trade data for AT %s from repository", tradeBotData.getAtAddress()));
+ return;
+ }
+ }
+ }
+
+ switch (tradeBotState) {
+ case BOB_WAITING_FOR_AT_CONFIRM:
+ handleBobWaitingForAtConfirm(repository, tradeBotData);
+ break;
+
+ case BOB_WAITING_FOR_MESSAGE:
+ TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
+ handleBobWaitingForMessage(repository, tradeBotData, atData, tradeData);
+ break;
+
+ case ALICE_WAITING_FOR_AT_LOCK:
+ TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
+ handleAliceWaitingForAtLock(repository, tradeBotData, atData, tradeData);
+ break;
+
+ case BOB_WAITING_FOR_AT_REDEEM:
+ TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
+ handleBobWaitingForAtRedeem(repository, tradeBotData, atData, tradeData);
+ break;
+
+ case ALICE_DONE:
+ case BOB_DONE:
+ break;
+
+ case ALICE_REFUNDING_A:
+ TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
+ handleAliceRefundingP2shA(repository, tradeBotData, atData, tradeData);
+ break;
+
+ case ALICE_REFUNDED:
+ case BOB_REFUNDED:
+ break;
+ }
+ }
+
+ /**
+ * 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())) {
+ if (NTP.getTime() - tradeBotData.getTimestamp() <= MAX_AT_CONFIRMATION_PERIOD)
+ return;
+
+ // We've waited ages for AT to be confirmed into a block but something has gone awry.
+ // After this long we assume transaction loss so give up with trade-bot entry too.
+ tradeBotData.setState(State.BOB_REFUNDED.name());
+ tradeBotData.setStateValue(State.BOB_REFUNDED.value);
+ tradeBotData.setTimestamp(NTP.getTime());
+ // We delete trade-bot entry here instead of saving, hence not using updateTradeBotState()
+ repository.getCrossChainRepository().delete(tradeBotData.getTradePrivateKey());
+ repository.saveChanges();
+
+ LOGGER.info(() -> String.format("AT %s never confirmed. Giving up on trade", tradeBotData.getAtAddress()));
+ TradeBot.notifyStateChange(tradeBotData);
+ return;
+ }
+
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_WAITING_FOR_MESSAGE,
+ () -> String.format("AT %s confirmed ready. Waiting for trade message", 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 Ravencoin 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 Alice to redeem the AT, which will allow Bob to
+ * extract secret-A needed to redeem Alice's P2SH.
+ * @throws ForeignBlockchainException
+ */
+ private void handleBobWaitingForMessage(Repository repository, TradeBotData tradeBotData,
+ ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
+ // If AT has finished then Bob likely cancelled his trade offer
+ if (atData.getIsFinished()) {
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED,
+ () -> String.format("AT %s cancelled - trading aborted", tradeBotData.getAtAddress()));
+ return;
+ }
+
+ Ravencoin ravencoin = Ravencoin.getInstance();
+
+ String address = tradeBotData.getTradeNativeAddress();
+ List messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, address, null, null, null);
+
+ for (MessageTransactionData messageTransactionData : messageTransactionsData) {
+ if (messageTransactionData.isText())
+ continue;
+
+ // We're expecting: HASH160(secret-A), Alice's Ravencoin pubkeyhash and lockTime-A
+ byte[] messageData = messageTransactionData.getData();
+ RavencoinACCTv3.OfferMessageData offerMessageData = RavencoinACCTv3.extractOfferMessageData(messageData);
+ if (offerMessageData == null)
+ continue;
+
+ byte[] aliceForeignPublicKeyHash = offerMessageData.partnerRavencoinPKH;
+ byte[] hashOfSecretA = offerMessageData.hashOfSecretA;
+ int lockTimeA = (int) offerMessageData.lockTimeA;
+ long messageTimestamp = messageTransactionData.getTimestamp();
+ int refundTimeout = RavencoinACCTv3.calcRefundTimeout(messageTimestamp, lockTimeA);
+
+ // Determine P2SH-A address and confirm funded
+ byte[] redeemScriptA = BitcoinyHTLC.buildScript(aliceForeignPublicKeyHash, lockTimeA, tradeBotData.getTradeForeignPublicKeyHash(), hashOfSecretA);
+ String p2shAddressA = ravencoin.deriveP2shAddress(redeemScriptA);
+
+ long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
+ long p2shFee = Ravencoin.getInstance().getP2shFee(feeTimestamp);
+ final long minimumAmountA = tradeBotData.getForeignAmount() + p2shFee;
+
+ BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(ravencoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
+
+ switch (htlcStatusA) {
+ case UNFUNDED:
+ case FUNDING_IN_PROGRESS:
+ // There might be another MESSAGE from someone else with an actually funded P2SH-A...
+ continue;
+
+ case REDEEM_IN_PROGRESS:
+ 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));
+ return;
+
+ case REFUND_IN_PROGRESS:
+ case REFUNDED:
+ // This P2SH-A is burnt, but there might be another MESSAGE from someone else with an actually funded P2SH-A...
+ continue;
+
+ case FUNDED:
+ // Fall-through out of switch...
+ break;
+ }
+
+ // Good to go - send MESSAGE to AT
+
+ String aliceNativeAddress = Crypto.toAddress(messageTransactionData.getCreatorPublicKey());
+
+ // Build outgoing message, padding each part to 32 bytes to make it easier for AT to consume
+ byte[] outgoingMessageData = RavencoinACCTv3.buildTradeMessage(aliceNativeAddress, aliceForeignPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
+ String messageRecipient = tradeBotData.getAtAddress();
+
+ boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, outgoingMessageData);
+ if (!isMessageAlreadySent) {
+ PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
+ MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false);
+
+ outgoingMessageTransaction.computeNonce();
+ outgoingMessageTransaction.sign(sender);
+
+ // reset repository state to prevent deadlock
+ repository.discardChanges();
+ ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
+
+ if (result != ValidationResult.OK) {
+ LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
+ return;
+ }
+ }
+
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_WAITING_FOR_AT_REDEEM,
+ () -> String.format("Locked AT %s to %s. Waiting for AT redeem", tradeBotData.getAtAddress(), aliceNativeAddress));
+
+ return;
+ }
+ }
+
+ /**
+ * 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 redeems AT using Alice's secret-A, releasing Bob's QORT to Alice.
+ *
+ * In revealing a valid secret-A, Bob can then redeem the RVN funds from P2SH-A.
+ *
+ * @throws ForeignBlockchainException
+ */
+ private void handleAliceWaitingForAtLock(Repository repository, TradeBotData tradeBotData,
+ ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
+ if (aliceUnexpectedState(repository, tradeBotData, atData, crossChainTradeData))
+ return;
+
+ Ravencoin ravencoin = Ravencoin.getInstance();
+ int lockTimeA = tradeBotData.getLockTimeA();
+
+ // Refund P2SH-A if we've passed lockTime-A
+ if (NTP.getTime() >= lockTimeA * 1000L) {
+ byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
+ String p2shAddressA = ravencoin.deriveP2shAddress(redeemScriptA);
+
+ long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
+ long p2shFee = Ravencoin.getInstance().getP2shFee(feeTimestamp);
+ long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
+
+ BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(ravencoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
+
+ switch (htlcStatusA) {
+ case UNFUNDED:
+ case FUNDING_IN_PROGRESS:
+ case FUNDED:
+ break;
+
+ case REDEEM_IN_PROGRESS:
+ case REDEEMED:
+ // Already redeemed?
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
+ () -> String.format("P2SH-A %s already spent? Assuming trade completed", p2shAddressA));
+ return;
+
+ case REFUND_IN_PROGRESS:
+ case REFUNDED:
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED,
+ () -> String.format("P2SH-A %s already refunded. Trade aborted", p2shAddressA));
+ 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));
+
+ return;
+ }
+
+ // We're waiting for AT to be in TRADE mode
+ if (crossChainTradeData.mode != AcctMode.TRADING)
+ return;
+
+ // AT is in TRADE mode and locked to us as checked by aliceUnexpectedState() above
+
+ // Find our MESSAGE to AT from previous state
+ List messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(tradeBotData.getTradeNativePublicKey(),
+ crossChainTradeData.qortalCreatorTradeAddress, null, null, null);
+ if (messageTransactionsData == null || messageTransactionsData.isEmpty()) {
+ LOGGER.warn(() -> String.format("Unable to find our message to trade creator %s?", crossChainTradeData.qortalCreatorTradeAddress));
+ return;
+ }
+
+ long recipientMessageTimestamp = messageTransactionsData.get(0).getTimestamp();
+ int refundTimeout = RavencoinACCTv3.calcRefundTimeout(recipientMessageTimestamp, lockTimeA);
+
+ // Our calculated refundTimeout should match AT's refundTimeout
+ if (refundTimeout != crossChainTradeData.refundTimeout) {
+ LOGGER.debug(() -> String.format("Trade AT refundTimeout '%d' doesn't match our refundTimeout '%d'", crossChainTradeData.refundTimeout, refundTimeout));
+ // We'll eventually refund
+ return;
+ }
+
+ // We're good to redeem AT
+
+ // Send 'redeem' MESSAGE to AT using both secret
+ byte[] secretA = tradeBotData.getSecret();
+ String qortalReceivingAddress = Base58.encode(tradeBotData.getReceivingAccountInfo()); // Actually contains whole address, not just PKH
+ byte[] messageData = RavencoinACCTv3.buildRedeemMessage(secretA, qortalReceivingAddress);
+ String messageRecipient = tradeBotData.getAtAddress();
+
+ boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
+ if (!isMessageAlreadySent) {
+ PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
+ MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
+
+ messageTransaction.computeNonce();
+ messageTransaction.sign(sender);
+
+ // Reset repository state to prevent deadlock
+ repository.discardChanges();
+ ValidationResult result = messageTransaction.importAsUnconfirmed();
+
+ if (result != ValidationResult.OK) {
+ LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
+ return;
+ }
+ }
+
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
+ () -> String.format("Redeeming AT %s. Funds should arrive at %s",
+ tradeBotData.getAtAddress(), qortalReceivingAddress));
+ }
+
+ /**
+ * Trade-bot is waiting for Alice to redeem Bob's AT, thus revealing secret-A which is required to spend the RVN 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 RVN funds from P2SH-A
+ * to Bob's 'foreign'/Ravencoin trade legacy-format address, as derived from trade private key.
+ *
+ * (This could potentially be 'improved' to send RVN 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.
+ * @throws ForeignBlockchainException
+ */
+ private void handleBobWaitingForAtRedeem(Repository repository, TradeBotData tradeBotData,
+ ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
+ // AT should be 'finished' once Alice has redeemed QORT funds
+ if (!atData.getIsFinished())
+ // Not finished yet
+ return;
+
+ // If AT is REFUNDED or CANCELLED then something has gone wrong
+ if (crossChainTradeData.mode == AcctMode.REFUNDED || crossChainTradeData.mode == AcctMode.CANCELLED) {
+ // Alice hasn't redeemed the QORT, so there is no point in trying to redeem the RVN
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED,
+ () -> String.format("AT %s has auto-refunded - trade aborted", tradeBotData.getAtAddress()));
+
+ return;
+ }
+
+ byte[] secretA = RavencoinACCTv3.getInstance().findSecretA(repository, crossChainTradeData);
+ if (secretA == null) {
+ LOGGER.debug(() -> String.format("Unable to find secret-A from redeem message to AT %s?", tradeBotData.getAtAddress()));
+ return;
+ }
+
+ // Use secret-A to redeem P2SH-A
+
+ Ravencoin ravencoin = Ravencoin.getInstance();
+
+ byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo();
+ int lockTimeA = crossChainTradeData.lockTimeA;
+ byte[] redeemScriptA = BitcoinyHTLC.buildScript(crossChainTradeData.partnerForeignPKH, lockTimeA, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretA);
+ String p2shAddressA = ravencoin.deriveP2shAddress(redeemScriptA);
+
+ // Fee for redeem/refund is subtracted from P2SH-A balance.
+ long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
+ long p2shFee = Ravencoin.getInstance().getP2shFee(feeTimestamp);
+ long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
+ BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(ravencoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
+
+ switch (htlcStatusA) {
+ case UNFUNDED:
+ case FUNDING_IN_PROGRESS:
+ // P2SH-A suddenly not funded? Our best bet at this point is to hope for AT auto-refund
+ return;
+
+ case REDEEM_IN_PROGRESS:
+ case REDEEMED:
+ // Double-check that we have redeemed P2SH-A...
+ break;
+
+ case REFUND_IN_PROGRESS:
+ case REFUNDED:
+ // Wait for AT to auto-refund
+ return;
+
+ case FUNDED: {
+ Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
+ ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
+ List fundingOutputs = ravencoin.getUnspentOutputs(p2shAddressA);
+
+ Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(ravencoin.getNetworkParameters(), redeemAmount, redeemKey,
+ fundingOutputs, redeemScriptA, secretA, receivingAccountInfo);
+
+ ravencoin.broadcastTransaction(p2shRedeemTransaction);
+ break;
+ }
+ }
+
+ String receivingAddress = ravencoin.pkhToAddress(receivingAccountInfo);
+
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_DONE,
+ () -> String.format("P2SH-A %s redeemed. Funds should arrive at %s", tradeBotData.getAtAddress(), receivingAddress));
+ }
+
+ /**
+ * Trade-bot is attempting to refund P2SH-A.
+ * @throws ForeignBlockchainException
+ */
+ private void handleAliceRefundingP2shA(Repository repository, TradeBotData tradeBotData,
+ ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
+ int lockTimeA = tradeBotData.getLockTimeA();
+
+ // We can't refund P2SH-A until lockTime-A has passed
+ if (NTP.getTime() <= lockTimeA * 1000L)
+ return;
+
+ Ravencoin ravencoin = Ravencoin.getInstance();
+
+ // We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
+ int medianBlockTime = ravencoin.getMedianBlockTime();
+ if (medianBlockTime <= lockTimeA)
+ return;
+
+ byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
+ String p2shAddressA = ravencoin.deriveP2shAddress(redeemScriptA);
+
+ // Fee for redeem/refund is subtracted from P2SH-A balance.
+ long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
+ long p2shFee = Ravencoin.getInstance().getP2shFee(feeTimestamp);
+ long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
+ BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(ravencoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
+
+ switch (htlcStatusA) {
+ case UNFUNDED:
+ case FUNDING_IN_PROGRESS:
+ // Still waiting for P2SH-A to be funded...
+ return;
+
+ case REDEEM_IN_PROGRESS:
+ case REDEEMED:
+ // Too late!
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
+ () -> String.format("P2SH-A %s already spent!", p2shAddressA));
+ return;
+
+ case REFUND_IN_PROGRESS:
+ case REFUNDED:
+ break;
+
+ case FUNDED:{
+ Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
+ ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
+ List fundingOutputs = ravencoin.getUnspentOutputs(p2shAddressA);
+
+ // Determine receive address for refund
+ String receiveAddress = ravencoin.getUnusedReceiveAddress(tradeBotData.getForeignKey());
+ Address receiving = Address.fromString(ravencoin.getNetworkParameters(), receiveAddress);
+
+ Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(ravencoin.getNetworkParameters(), refundAmount, refundKey,
+ fundingOutputs, redeemScriptA, lockTimeA, receiving.getHash());
+
+ ravencoin.broadcastTransaction(p2shRefundTransaction);
+ break;
+ }
+ }
+
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED,
+ () -> String.format("LockTime-A reached. Refunded P2SH-A %s. Trade aborted", p2shAddressA));
+ }
+
+ /**
+ * Returns true if Alice finds AT unexpectedly cancelled, refunded, redeemed or locked to someone else.
+ *
+ * Will automatically update trade-bot state to ALICE_REFUNDING_A or ALICE_DONE as necessary.
+ *
+ * @throws DataException
+ * @throws ForeignBlockchainException
+ */
+ private boolean aliceUnexpectedState(Repository repository, TradeBotData tradeBotData,
+ ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
+ // This is OK
+ if (!atData.getIsFinished() && crossChainTradeData.mode == AcctMode.OFFERING)
+ return false;
+
+ boolean isAtLockedToUs = tradeBotData.getTradeNativeAddress().equals(crossChainTradeData.qortalPartnerAddress);
+
+ if (!atData.getIsFinished() && crossChainTradeData.mode == AcctMode.TRADING)
+ if (isAtLockedToUs) {
+ // AT is trading with us - OK
+ return false;
+ } else {
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
+ () -> String.format("AT %s trading with someone else: %s. Refunding & aborting trade", tradeBotData.getAtAddress(), crossChainTradeData.qortalPartnerAddress));
+
+ return true;
+ }
+
+ if (atData.getIsFinished() && crossChainTradeData.mode == AcctMode.REDEEMED && isAtLockedToUs) {
+ // We've redeemed already?
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
+ () -> String.format("AT %s already redeemed by us. Trade completed", tradeBotData.getAtAddress()));
+ } else {
+ // Any other state is not good, so start defensive refund
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
+ () -> String.format("AT %s cancelled/refunded/redeemed by someone else/invalid state. Refunding & aborting trade", tradeBotData.getAtAddress()));
+ }
+
+ return true;
+ }
+
+ private long calcFeeTimestamp(int lockTimeA, int tradeTimeout) {
+ return (lockTimeA - tradeTimeout * 60) * 1000L;
+ }
+
+}
diff --git a/src/main/java/org/qortal/controller/tradebot/TradeBot.java b/src/main/java/org/qortal/controller/tradebot/TradeBot.java
index 6d7ac942..e1021f6c 100644
--- a/src/main/java/org/qortal/controller/tradebot/TradeBot.java
+++ b/src/main/java/org/qortal/controller/tradebot/TradeBot.java
@@ -100,6 +100,8 @@ public class TradeBot implements Listener {
acctTradeBotSuppliers.put(DogecoinACCTv1.class, DogecoinACCTv1TradeBot::getInstance);
acctTradeBotSuppliers.put(DogecoinACCTv2.class, DogecoinACCTv2TradeBot::getInstance);
acctTradeBotSuppliers.put(DogecoinACCTv3.class, DogecoinACCTv3TradeBot::getInstance);
+ acctTradeBotSuppliers.put(DigibyteACCTv3.class, DigibyteACCTv3TradeBot::getInstance);
+ acctTradeBotSuppliers.put(RavencoinACCTv3.class, RavencoinACCTv3TradeBot::getInstance);
}
private static TradeBot instance;
diff --git a/src/main/java/org/qortal/crosschain/Digibyte.java b/src/main/java/org/qortal/crosschain/Digibyte.java
new file mode 100644
index 00000000..3ab5e78e
--- /dev/null
+++ b/src/main/java/org/qortal/crosschain/Digibyte.java
@@ -0,0 +1,171 @@
+package org.qortal.crosschain;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.EnumMap;
+import java.util.Map;
+
+import org.bitcoinj.core.Coin;
+import org.bitcoinj.core.Context;
+import org.bitcoinj.core.NetworkParameters;
+import org.bitcoinj.params.RegTestParams;
+import org.bitcoinj.params.TestNet3Params;
+import org.libdohj.params.DigibyteMainNetParams;
+import org.qortal.crosschain.ElectrumX.Server;
+import org.qortal.crosschain.ElectrumX.Server.ConnectionType;
+import org.qortal.settings.Settings;
+
+public class Digibyte extends Bitcoiny {
+
+ public static final String CURRENCY_CODE = "DGB";
+
+ private static final Coin DEFAULT_FEE_PER_KB = Coin.valueOf(100000); // 0.001 DGB per 1000 bytes
+
+ private static final long MINIMUM_ORDER_AMOUNT = 1000000; // 0.01 DGB minimum order, to avoid dust errors
+
+ // Temporary values until a dynamic fee system is written.
+ private static final long MAINNET_FEE = 10000L;
+ private static final long NON_MAINNET_FEE = 10000L; // enough for TESTNET3 and should be OK for REGTEST
+
+ private static final Map DEFAULT_ELECTRUMX_PORTS = new EnumMap<>(ConnectionType.class);
+ static {
+ DEFAULT_ELECTRUMX_PORTS.put(ConnectionType.TCP, 50001);
+ DEFAULT_ELECTRUMX_PORTS.put(ConnectionType.SSL, 50002);
+ }
+
+ public enum DigibyteNet {
+ MAIN {
+ @Override
+ public NetworkParameters getParams() {
+ return DigibyteMainNetParams.get();
+ }
+
+ @Override
+ public Collection getServers() {
+ return Arrays.asList(
+ // Servers chosen on NO BASIS WHATSOEVER from various sources!
+ // Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=dgb
+ new Server("electrum1.cipig.net", ConnectionType.SSL, 20059),
+ new Server("electrum2.cipig.net", ConnectionType.SSL, 20059),
+ new Server("electrum3.cipig.net", ConnectionType.SSL, 20059));
+ }
+
+ @Override
+ public String getGenesisHash() {
+ return "7497ea1b465eb39f1c8f507bc877078fe016d6fcb6dfad3a64c98dcc6e1e8496";
+ }
+
+ @Override
+ public long getP2shFee(Long timestamp) {
+ // TODO: This will need to be replaced with something better in the near future!
+ return MAINNET_FEE;
+ }
+ },
+ TEST3 {
+ @Override
+ public NetworkParameters getParams() {
+ return TestNet3Params.get();
+ }
+
+ @Override
+ public Collection getServers() {
+ return Arrays.asList(); // TODO: find testnet servers
+ }
+
+ @Override
+ public String getGenesisHash() {
+ return "308ea0711d5763be2995670dd9ca9872753561285a84da1d58be58acaa822252";
+ }
+
+ @Override
+ public long getP2shFee(Long timestamp) {
+ return NON_MAINNET_FEE;
+ }
+ },
+ REGTEST {
+ @Override
+ public NetworkParameters getParams() {
+ return RegTestParams.get();
+ }
+
+ @Override
+ public Collection getServers() {
+ return Arrays.asList(
+ new Server("localhost", ConnectionType.TCP, 50001),
+ new Server("localhost", ConnectionType.SSL, 50002));
+ }
+
+ @Override
+ public String getGenesisHash() {
+ // This is unique to each regtest instance
+ return null;
+ }
+
+ @Override
+ public long getP2shFee(Long timestamp) {
+ return NON_MAINNET_FEE;
+ }
+ };
+
+ public abstract NetworkParameters getParams();
+ public abstract Collection getServers();
+ public abstract String getGenesisHash();
+ public abstract long getP2shFee(Long timestamp) throws ForeignBlockchainException;
+ }
+
+ private static Digibyte instance;
+
+ private final DigibyteNet digibyteNet;
+
+ // Constructors and instance
+
+ private Digibyte(DigibyteNet digibyteNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
+ super(blockchain, bitcoinjContext, currencyCode);
+ this.digibyteNet = digibyteNet;
+
+ LOGGER.info(() -> String.format("Starting Digibyte support using %s", this.digibyteNet.name()));
+ }
+
+ public static synchronized Digibyte getInstance() {
+ if (instance == null) {
+ DigibyteNet digibyteNet = Settings.getInstance().getDigibyteNet();
+
+ BitcoinyBlockchainProvider electrumX = new ElectrumX("Digibyte-" + digibyteNet.name(), digibyteNet.getGenesisHash(), digibyteNet.getServers(), DEFAULT_ELECTRUMX_PORTS);
+ Context bitcoinjContext = new Context(digibyteNet.getParams());
+
+ instance = new Digibyte(digibyteNet, electrumX, bitcoinjContext, CURRENCY_CODE);
+ }
+
+ return instance;
+ }
+
+ // Getters & setters
+
+ public static synchronized void resetForTesting() {
+ instance = null;
+ }
+
+ // Actual useful methods for use by other classes
+
+ @Override
+ public Coin getFeePerKb() {
+ return DEFAULT_FEE_PER_KB;
+ }
+
+ @Override
+ public long getMinimumOrderAmount() {
+ return MINIMUM_ORDER_AMOUNT;
+ }
+
+ /**
+ * Returns estimated DGB fee, in sats per 1000bytes, optionally for historic timestamp.
+ *
+ * @param timestamp optional milliseconds since epoch, or null for 'now'
+ * @return sats per 1000bytes, or throws ForeignBlockchainException if something went wrong
+ */
+ @Override
+ public long getP2shFee(Long timestamp) throws ForeignBlockchainException {
+ return this.digibyteNet.getP2shFee(timestamp);
+ }
+
+}
diff --git a/src/main/java/org/qortal/crosschain/DigibyteACCTv3.java b/src/main/java/org/qortal/crosschain/DigibyteACCTv3.java
new file mode 100644
index 00000000..e1e33862
--- /dev/null
+++ b/src/main/java/org/qortal/crosschain/DigibyteACCTv3.java
@@ -0,0 +1,858 @@
+package org.qortal.crosschain;
+
+import com.google.common.hash.HashCode;
+import com.google.common.primitives.Bytes;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.ciyam.at.*;
+import org.qortal.account.Account;
+import org.qortal.asset.Asset;
+import org.qortal.at.QortalFunctionCode;
+import org.qortal.crypto.Crypto;
+import org.qortal.data.at.ATData;
+import org.qortal.data.at.ATStateData;
+import org.qortal.data.crosschain.CrossChainTradeData;
+import org.qortal.data.transaction.MessageTransactionData;
+import org.qortal.repository.DataException;
+import org.qortal.repository.Repository;
+import org.qortal.utils.Base58;
+import org.qortal.utils.BitTwiddling;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.ciyam.at.OpCode.calcOffset;
+
+/**
+ * Cross-chain trade AT
+ *
+ *
+ *
+ * - Bob generates Digibyte & Qortal 'trade' keys
+ *
+ * - private key required to sign P2SH redeem tx
+ * - private key could be used to create 'secret' (e.g. double-SHA256)
+ * - encrypted private key could be stored in Qortal AT for access by Bob from any node
+ *
+ *
+ * - Bob deploys Qortal AT
+ *
+ *
+ * - Alice finds Qortal AT and wants to trade
+ *
+ * - Alice generates Digibyte & Qortal 'trade' keys
+ * - Alice funds Digibyte P2SH-A
+ * - Alice sends 'offer' MESSAGE to Bob from her Qortal trade address, containing:
+ *
+ * - hash-of-secret-A
+ * - her 'trade' Digibyte PKH
+ *
+ *
+ *
+ *
+ * - Bob receives "offer" MESSAGE
+ *
+ * - Checks Alice's P2SH-A
+ * - Sends 'trade' MESSAGE to Qortal AT from his trade address, containing:
+ *
+ * - Alice's trade Qortal address
+ * - Alice's trade Digibyte PKH
+ * - hash-of-secret-A
+ *
+ *
+ *
+ *
+ * - Alice checks Qortal AT to confirm it's locked to her
+ *
+ * - Alice sends 'redeem' MESSAGE to Qortal AT from her trade address, containing:
+ *
+ * - secret-A
+ * - Qortal receiving address of her chosing
+ *
+ *
+ * - AT's QORT funds are sent to Qortal receiving address
+ *
+ *
+ * - Bob checks AT, extracts secret-A
+ *
+ * - Bob redeems P2SH-A using his Digibyte trade key and secret-A
+ * - P2SH-A DGB funds end up at Digibyte address determined by redeem transaction output(s)
+ *
+ *
+ *
+ */
+public class DigibyteACCTv3 implements ACCT {
+
+ private static final Logger LOGGER = LogManager.getLogger(DigibyteACCTv3.class);
+
+ public static final String NAME = DigibyteACCTv3.class.getSimpleName();
+ public static final byte[] CODE_BYTES_HASH = HashCode.fromString("e6a7dcd87296fae3ce7d80183bf7660c8e2cb4f8746c6a0421a17148f87a0e1d").asBytes(); // SHA256 of AT code bytes
+
+ public static final int SECRET_LENGTH = 32;
+
+ /** Value offset into AT segment where 'mode' variable (long) is stored. (Multiply by MachineState.VALUE_SIZE for byte offset). */
+ private static final int MODE_VALUE_OFFSET = 61;
+ /** Byte offset into AT state data where 'mode' variable (long) is stored. */
+ public static final int MODE_BYTE_OFFSET = MachineState.HEADER_LENGTH + (MODE_VALUE_OFFSET * MachineState.VALUE_SIZE);
+
+ public static class OfferMessageData {
+ public byte[] partnerDigibytePKH;
+ public byte[] hashOfSecretA;
+ public long lockTimeA;
+ }
+ public static final int OFFER_MESSAGE_LENGTH = 20 /*partnerDigibytePKH*/ + 20 /*hashOfSecretA*/ + 8 /*lockTimeA*/;
+ public static final int TRADE_MESSAGE_LENGTH = 32 /*partner's Qortal trade address (padded from 25 to 32)*/
+ + 24 /*partner's Digibyte PKH (padded from 20 to 24)*/
+ + 8 /*AT trade timeout (minutes)*/
+ + 24 /*hash of secret-A (padded from 20 to 24)*/
+ + 8 /*lockTimeA*/;
+ public static final int REDEEM_MESSAGE_LENGTH = 32 /*secret-A*/ + 32 /*partner's Qortal receiving address padded from 25 to 32*/;
+ public static final int CANCEL_MESSAGE_LENGTH = 32 /*AT creator's Qortal address*/;
+
+ private static DigibyteACCTv3 instance;
+
+ private DigibyteACCTv3() {
+ }
+
+ public static synchronized DigibyteACCTv3 getInstance() {
+ if (instance == null)
+ instance = new DigibyteACCTv3();
+
+ return instance;
+ }
+
+ @Override
+ public byte[] getCodeBytesHash() {
+ return CODE_BYTES_HASH;
+ }
+
+ @Override
+ public int getModeByteOffset() {
+ return MODE_BYTE_OFFSET;
+ }
+
+ @Override
+ public ForeignBlockchain getBlockchain() {
+ return Digibyte.getInstance();
+ }
+
+ /**
+ * Returns Qortal AT creation bytes for cross-chain trading AT.
+ *
+ * tradeTimeout (minutes) is the time window for the trade partner to send the
+ * 32-byte secret to the AT, before the AT automatically refunds the AT's creator.
+ *
+ * @param creatorTradeAddress AT creator's trade Qortal address
+ * @param digibytePublicKeyHash 20-byte HASH160 of creator's trade Digibyte public key
+ * @param qortAmount how much QORT to pay trade partner if they send correct 32-byte secrets to AT
+ * @param digibyteAmount how much DGB the AT creator is expecting to trade
+ * @param tradeTimeout suggested timeout for entire trade
+ */
+ public static byte[] buildQortalAT(String creatorTradeAddress, byte[] digibytePublicKeyHash, long qortAmount, long digibyteAmount, int tradeTimeout) {
+ if (digibytePublicKeyHash.length != 20)
+ throw new IllegalArgumentException("Digibyte public key hash should be 20 bytes");
+
+ // Labels for data segment addresses
+ int addrCounter = 0;
+
+ // Constants (with corresponding dataByteBuffer.put*() calls below)
+
+ final int addrCreatorTradeAddress1 = addrCounter++;
+ final int addrCreatorTradeAddress2 = addrCounter++;
+ final int addrCreatorTradeAddress3 = addrCounter++;
+ final int addrCreatorTradeAddress4 = addrCounter++;
+
+ final int addrDigibytePublicKeyHash = addrCounter;
+ addrCounter += 4;
+
+ final int addrQortAmount = addrCounter++;
+ final int addrDigibyteAmount = addrCounter++;
+ final int addrTradeTimeout = addrCounter++;
+
+ final int addrMessageTxnType = addrCounter++;
+ final int addrExpectedTradeMessageLength = addrCounter++;
+ final int addrExpectedRedeemMessageLength = addrCounter++;
+
+ final int addrCreatorAddressPointer = addrCounter++;
+ final int addrQortalPartnerAddressPointer = addrCounter++;
+ final int addrMessageSenderPointer = addrCounter++;
+
+ final int addrTradeMessagePartnerDigibytePKHOffset = addrCounter++;
+ final int addrPartnerDigibytePKHPointer = addrCounter++;
+ final int addrTradeMessageHashOfSecretAOffset = addrCounter++;
+ final int addrHashOfSecretAPointer = addrCounter++;
+
+ final int addrRedeemMessageReceivingAddressOffset = addrCounter++;
+
+ final int addrMessageDataPointer = addrCounter++;
+ final int addrMessageDataLength = addrCounter++;
+
+ final int addrPartnerReceivingAddressPointer = addrCounter++;
+
+ final int addrEndOfConstants = addrCounter;
+
+ // Variables
+
+ final int addrCreatorAddress1 = addrCounter++;
+ final int addrCreatorAddress2 = addrCounter++;
+ final int addrCreatorAddress3 = addrCounter++;
+ final int addrCreatorAddress4 = addrCounter++;
+
+ final int addrQortalPartnerAddress1 = addrCounter++;
+ final int addrQortalPartnerAddress2 = addrCounter++;
+ final int addrQortalPartnerAddress3 = addrCounter++;
+ final int addrQortalPartnerAddress4 = addrCounter++;
+
+ final int addrLockTimeA = addrCounter++;
+ final int addrRefundTimeout = addrCounter++;
+ final int addrRefundTimestamp = addrCounter++;
+ final int addrLastTxnTimestamp = addrCounter++;
+ final int addrBlockTimestamp = addrCounter++;
+ final int addrTxnType = addrCounter++;
+ final int addrResult = addrCounter++;
+
+ final int addrMessageSender1 = addrCounter++;
+ final int addrMessageSender2 = addrCounter++;
+ final int addrMessageSender3 = addrCounter++;
+ final int addrMessageSender4 = addrCounter++;
+
+ final int addrMessageLength = addrCounter++;
+
+ final int addrMessageData = addrCounter;
+ addrCounter += 4;
+
+ final int addrHashOfSecretA = addrCounter;
+ addrCounter += 4;
+
+ final int addrPartnerDigibytePKH = addrCounter;
+ addrCounter += 4;
+
+ final int addrPartnerReceivingAddress = addrCounter;
+ addrCounter += 4;
+
+ final int addrMode = addrCounter++;
+ assert addrMode == MODE_VALUE_OFFSET : String.format("addrMode %d does not match MODE_VALUE_OFFSET %d", addrMode, MODE_VALUE_OFFSET);
+
+ // Data segment
+ ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
+
+ // AT creator's trade Qortal address, decoded from Base58
+ assert dataByteBuffer.position() == addrCreatorTradeAddress1 * MachineState.VALUE_SIZE : "addrCreatorTradeAddress1 incorrect";
+ byte[] creatorTradeAddressBytes = Base58.decode(creatorTradeAddress);
+ dataByteBuffer.put(Bytes.ensureCapacity(creatorTradeAddressBytes, 32, 0));
+
+ // Digibyte public key hash
+ assert dataByteBuffer.position() == addrDigibytePublicKeyHash * MachineState.VALUE_SIZE : "addrDigibytePublicKeyHash incorrect";
+ dataByteBuffer.put(Bytes.ensureCapacity(digibytePublicKeyHash, 32, 0));
+
+ // Redeem Qort amount
+ assert dataByteBuffer.position() == addrQortAmount * MachineState.VALUE_SIZE : "addrQortAmount incorrect";
+ dataByteBuffer.putLong(qortAmount);
+
+ // Expected Digibyte amount
+ assert dataByteBuffer.position() == addrDigibyteAmount * MachineState.VALUE_SIZE : "addrDigibyteAmount incorrect";
+ dataByteBuffer.putLong(digibyteAmount);
+
+ // Suggested trade timeout (minutes)
+ assert dataByteBuffer.position() == addrTradeTimeout * MachineState.VALUE_SIZE : "addrTradeTimeout incorrect";
+ dataByteBuffer.putLong(tradeTimeout);
+
+ // We're only interested in MESSAGE transactions
+ assert dataByteBuffer.position() == addrMessageTxnType * MachineState.VALUE_SIZE : "addrMessageTxnType incorrect";
+ dataByteBuffer.putLong(API.ATTransactionType.MESSAGE.value);
+
+ // Expected length of 'trade' MESSAGE data from AT creator
+ assert dataByteBuffer.position() == addrExpectedTradeMessageLength * MachineState.VALUE_SIZE : "addrExpectedTradeMessageLength incorrect";
+ dataByteBuffer.putLong(TRADE_MESSAGE_LENGTH);
+
+ // Expected length of 'redeem' MESSAGE data from trade partner
+ assert dataByteBuffer.position() == addrExpectedRedeemMessageLength * MachineState.VALUE_SIZE : "addrExpectedRedeemMessageLength incorrect";
+ dataByteBuffer.putLong(REDEEM_MESSAGE_LENGTH);
+
+ // Index into data segment of AT creator's address, used by GET_B_IND
+ assert dataByteBuffer.position() == addrCreatorAddressPointer * MachineState.VALUE_SIZE : "addrCreatorAddressPointer incorrect";
+ dataByteBuffer.putLong(addrCreatorAddress1);
+
+ // Index into data segment of partner's Qortal address, used by SET_B_IND
+ assert dataByteBuffer.position() == addrQortalPartnerAddressPointer * MachineState.VALUE_SIZE : "addrQortalPartnerAddressPointer incorrect";
+ dataByteBuffer.putLong(addrQortalPartnerAddress1);
+
+ // Index into data segment of (temporary) transaction's sender's address, used by GET_B_IND
+ assert dataByteBuffer.position() == addrMessageSenderPointer * MachineState.VALUE_SIZE : "addrMessageSenderPointer incorrect";
+ dataByteBuffer.putLong(addrMessageSender1);
+
+ // Offset into 'trade' MESSAGE data payload for extracting partner's Digibyte PKH
+ assert dataByteBuffer.position() == addrTradeMessagePartnerDigibytePKHOffset * MachineState.VALUE_SIZE : "addrTradeMessagePartnerDigibytePKHOffset incorrect";
+ dataByteBuffer.putLong(32L);
+
+ // Index into data segment of partner's Digibyte PKH, used by GET_B_IND
+ assert dataByteBuffer.position() == addrPartnerDigibytePKHPointer * MachineState.VALUE_SIZE : "addrPartnerDigibytePKHPointer incorrect";
+ dataByteBuffer.putLong(addrPartnerDigibytePKH);
+
+ // Offset into 'trade' MESSAGE data payload for extracting hash-of-secret-A
+ assert dataByteBuffer.position() == addrTradeMessageHashOfSecretAOffset * MachineState.VALUE_SIZE : "addrTradeMessageHashOfSecretAOffset incorrect";
+ dataByteBuffer.putLong(64L);
+
+ // Index into data segment to hash of secret A, used by GET_B_IND
+ assert dataByteBuffer.position() == addrHashOfSecretAPointer * MachineState.VALUE_SIZE : "addrHashOfSecretAPointer incorrect";
+ dataByteBuffer.putLong(addrHashOfSecretA);
+
+ // Offset into 'redeem' MESSAGE data payload for extracting Qortal receiving address
+ assert dataByteBuffer.position() == addrRedeemMessageReceivingAddressOffset * MachineState.VALUE_SIZE : "addrRedeemMessageReceivingAddressOffset incorrect";
+ dataByteBuffer.putLong(32L);
+
+ // Source location and length for hashing any passed secret
+ assert dataByteBuffer.position() == addrMessageDataPointer * MachineState.VALUE_SIZE : "addrMessageDataPointer incorrect";
+ dataByteBuffer.putLong(addrMessageData);
+ assert dataByteBuffer.position() == addrMessageDataLength * MachineState.VALUE_SIZE : "addrMessageDataLength incorrect";
+ dataByteBuffer.putLong(32L);
+
+ // Pointer into data segment of where to save partner's receiving Qortal address, used by GET_B_IND
+ assert dataByteBuffer.position() == addrPartnerReceivingAddressPointer * MachineState.VALUE_SIZE : "addrPartnerReceivingAddressPointer incorrect";
+ dataByteBuffer.putLong(addrPartnerReceivingAddress);
+
+ assert dataByteBuffer.position() == addrEndOfConstants * MachineState.VALUE_SIZE : "dataByteBuffer position not at end of constants";
+
+ // Code labels
+ Integer labelRefund = null;
+
+ Integer labelTradeTxnLoop = null;
+ Integer labelCheckTradeTxn = null;
+ Integer labelCheckCancelTxn = null;
+ Integer labelNotTradeNorCancelTxn = null;
+ Integer labelCheckNonRefundTradeTxn = null;
+ Integer labelTradeTxnExtract = null;
+ Integer labelRedeemTxnLoop = null;
+ Integer labelCheckRedeemTxn = null;
+ Integer labelCheckRedeemTxnSender = null;
+ Integer labelPayout = null;
+
+ ByteBuffer codeByteBuffer = ByteBuffer.allocate(768);
+
+ // Two-pass version
+ for (int pass = 0; pass < 2; ++pass) {
+ codeByteBuffer.clear();
+
+ try {
+ /* Initialization */
+
+ // Use AT creation 'timestamp' as starting point for finding transactions sent to AT
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrLastTxnTimestamp));
+
+ /* NOP - to ensure DIGIBYTE ACCT is unique */
+ codeByteBuffer.put(OpCode.NOP.compile());
+
+ // Load B register with AT creator's address so we can save it into addrCreatorAddress1-4
+ codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_CREATOR_INTO_B));
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrCreatorAddressPointer));
+
+ // Set restart position to after this opcode
+ codeByteBuffer.put(OpCode.SET_PCS.compile());
+
+ /* Loop, waiting for message from AT creator's trade address containing trade partner details, or AT owner's address to cancel offer */
+
+ /* Transaction processing loop */
+ labelTradeTxnLoop = codeByteBuffer.position();
+
+ /* Sleep until message arrives */
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.SLEEP_UNTIL_MESSAGE.value, addrLastTxnTimestamp));
+
+ // Find next transaction (if any) to this AT since the last one (referenced by addrLastTxnTimestamp)
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxnTimestamp));
+ // If no transaction found, A will be zero. If A is zero, set addrResult to 1, otherwise 0.
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.CHECK_A_IS_ZERO, addrResult));
+ // If addrResult is zero (i.e. A is non-zero, transaction was found) then go check transaction
+ codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelCheckTradeTxn)));
+ // Stop and wait for next block
+ codeByteBuffer.put(OpCode.STP_IMD.compile());
+
+ /* Check transaction */
+ labelCheckTradeTxn = codeByteBuffer.position();
+
+ // Update our 'last found transaction's timestamp' using 'timestamp' from transaction
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A, addrLastTxnTimestamp));
+ // Extract transaction type (message/payment) from transaction and save type in addrTxnType
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TYPE_FROM_TX_IN_A, addrTxnType));
+ // If transaction type is not MESSAGE type then go look for another transaction
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxnType, addrMessageTxnType, calcOffset(codeByteBuffer, labelTradeTxnLoop)));
+
+ /* Check transaction's sender. We're expecting AT creator's trade address for 'trade' message, or AT creator's own address for 'cancel' message. */
+
+ // Extract sender address from transaction into B register
+ codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_ADDRESS_FROM_TX_IN_A_INTO_B));
+ // Save B register into data segment starting at addrMessageSender1 (as pointed to by addrMessageSenderPointer)
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageSenderPointer));
+ // Compare each part of message sender's address with AT creator's trade address. If they don't match, check for cancel situation.
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrCreatorTradeAddress1, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrCreatorTradeAddress2, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrCreatorTradeAddress3, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrCreatorTradeAddress4, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
+ // Message sender's address matches AT creator's trade address so go process 'trade' message
+ codeByteBuffer.put(OpCode.JMP_ADR.compile(labelCheckNonRefundTradeTxn == null ? 0 : labelCheckNonRefundTradeTxn));
+
+ /* Checking message sender for possible cancel message */
+ labelCheckCancelTxn = codeByteBuffer.position();
+
+ // Compare each part of message sender's address with AT creator's address. If they don't match, look for another transaction.
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrCreatorAddress1, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrCreatorAddress2, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrCreatorAddress3, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrCreatorAddress4, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
+ // Partner address is AT creator's address, so cancel offer and finish.
+ codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.CANCELLED.value));
+ // We're finished forever (finishing auto-refunds remaining balance to AT creator)
+ codeByteBuffer.put(OpCode.FIN_IMD.compile());
+
+ /* Not trade nor cancel message */
+ labelNotTradeNorCancelTxn = codeByteBuffer.position();
+
+ // Loop to find another transaction
+ codeByteBuffer.put(OpCode.JMP_ADR.compile(labelTradeTxnLoop == null ? 0 : labelTradeTxnLoop));
+
+ /* Possible switch-to-trade-mode message */
+ labelCheckNonRefundTradeTxn = codeByteBuffer.position();
+
+ // Check 'trade' message we received has expected number of message bytes
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(QortalFunctionCode.GET_MESSAGE_LENGTH_FROM_TX_IN_A.value, addrMessageLength));
+ // If message length matches, branch to info extraction code
+ codeByteBuffer.put(OpCode.BEQ_DAT.compile(addrMessageLength, addrExpectedTradeMessageLength, calcOffset(codeByteBuffer, labelTradeTxnExtract)));
+ // Message length didn't match - go back to finding another 'trade' MESSAGE transaction
+ codeByteBuffer.put(OpCode.JMP_ADR.compile(labelTradeTxnLoop == null ? 0 : labelTradeTxnLoop));
+
+ /* Extracting info from 'trade' MESSAGE transaction */
+ labelTradeTxnExtract = codeByteBuffer.position();
+
+ // Extract message from transaction into B register
+ codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B));
+ // Save B register into data segment starting at addrQortalPartnerAddress1 (as pointed to by addrQortalPartnerAddressPointer)
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrQortalPartnerAddressPointer));
+
+ // Extract trade partner's Digibyte public key hash (PKH) from message into B
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessagePartnerDigibytePKHOffset));
+ // Store partner's Digibyte PKH (we only really use values from B1-B3)
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerDigibytePKHPointer));
+ // Extract AT trade timeout (minutes) (from B4)
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B4, addrRefundTimeout));
+
+ // Grab next 32 bytes
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessageHashOfSecretAOffset));
+
+ // Extract hash-of-secret-A (we only really use values from B1-B3)
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrHashOfSecretAPointer));
+ // Extract lockTime-A (from B4)
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B4, addrLockTimeA));
+
+ // Calculate trade timeout refund 'timestamp' by adding addrRefundTimeout minutes to this transaction's 'timestamp', then save into addrRefundTimestamp
+ codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.ADD_MINUTES_TO_TIMESTAMP, addrRefundTimestamp, addrLastTxnTimestamp, addrRefundTimeout));
+
+ /* We are in 'trade mode' */
+ codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.TRADING.value));
+
+ // Set restart position to after this opcode
+ codeByteBuffer.put(OpCode.SET_PCS.compile());
+
+ /* Loop, waiting for trade timeout or 'redeem' MESSAGE from Qortal trade partner */
+
+ // Fetch current block 'timestamp'
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_BLOCK_TIMESTAMP, addrBlockTimestamp));
+ // If we're not past refund 'timestamp' then look for next transaction
+ codeByteBuffer.put(OpCode.BLT_DAT.compile(addrBlockTimestamp, addrRefundTimestamp, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
+ // We're past refund 'timestamp' so go refund everything back to AT creator
+ codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRefund == null ? 0 : labelRefund));
+
+ /* Transaction processing loop */
+ labelRedeemTxnLoop = codeByteBuffer.position();
+
+ // Find next transaction to this AT since the last one (if any)
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxnTimestamp));
+ // If no transaction found, A will be zero. If A is zero, set addrComparator to 1, otherwise 0.
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.CHECK_A_IS_ZERO, addrResult));
+ // If addrResult is zero (i.e. A is non-zero, transaction was found) then go check transaction
+ codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelCheckRedeemTxn)));
+ // Stop and wait for next block
+ codeByteBuffer.put(OpCode.STP_IMD.compile());
+
+ /* Check transaction */
+ labelCheckRedeemTxn = codeByteBuffer.position();
+
+ // Update our 'last found transaction's timestamp' using 'timestamp' from transaction
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A, addrLastTxnTimestamp));
+ // Extract transaction type (message/payment) from transaction and save type in addrTxnType
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TYPE_FROM_TX_IN_A, addrTxnType));
+ // If transaction type is not MESSAGE type then go look for another transaction
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxnType, addrMessageTxnType, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
+
+ /* Check message payload length */
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(QortalFunctionCode.GET_MESSAGE_LENGTH_FROM_TX_IN_A.value, addrMessageLength));
+ // If message length matches, branch to sender checking code
+ codeByteBuffer.put(OpCode.BEQ_DAT.compile(addrMessageLength, addrExpectedRedeemMessageLength, calcOffset(codeByteBuffer, labelCheckRedeemTxnSender)));
+ // Message length didn't match - go back to finding another 'redeem' MESSAGE transaction
+ codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRedeemTxnLoop == null ? 0 : labelRedeemTxnLoop));
+
+ /* Check transaction's sender */
+ labelCheckRedeemTxnSender = codeByteBuffer.position();
+
+ // Extract sender address from transaction into B register
+ codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_ADDRESS_FROM_TX_IN_A_INTO_B));
+ // Save B register into data segment starting at addrMessageSender1 (as pointed to by addrMessageSenderPointer)
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageSenderPointer));
+ // Compare each part of transaction's sender's address with expected address. If they don't match, look for another transaction.
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrQortalPartnerAddress1, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrQortalPartnerAddress2, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrQortalPartnerAddress3, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrQortalPartnerAddress4, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
+
+ /* Check 'secret-A' in transaction's message */
+
+ // Extract secret-A from first 32 bytes of message from transaction into B register
+ codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B));
+ // Save B register into data segment starting at addrMessageData (as pointed to by addrMessageDataPointer)
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageDataPointer));
+ // Load B register with expected hash result (as pointed to by addrHashOfSecretAPointer)
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrHashOfSecretAPointer));
+ // Perform HASH160 using source data at addrMessageData. (Location and length specified via addrMessageDataPointer and addrMessageDataLength).
+ // Save the equality result (1 if they match, 0 otherwise) into addrResult.
+ codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.CHECK_HASH160_WITH_B, addrResult, addrMessageDataPointer, addrMessageDataLength));
+ // If hashes don't match, addrResult will be zero so go find another transaction
+ codeByteBuffer.put(OpCode.BNZ_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelPayout)));
+ codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRedeemTxnLoop == null ? 0 : labelRedeemTxnLoop));
+
+ /* Success! Pay arranged amount to receiving address */
+ labelPayout = codeByteBuffer.position();
+
+ // Extract Qortal receiving address from next 32 bytes of message from transaction into B register
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrRedeemMessageReceivingAddressOffset));
+ // Save B register into data segment starting at addrPartnerReceivingAddress (as pointed to by addrPartnerReceivingAddressPointer)
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerReceivingAddressPointer));
+ // Pay AT's balance to receiving address
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PAY_TO_ADDRESS_IN_B, addrQortAmount));
+ // Set redeemed mode
+ codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.REDEEMED.value));
+ // We're finished forever (finishing auto-refunds remaining balance to AT creator)
+ codeByteBuffer.put(OpCode.FIN_IMD.compile());
+
+ // Fall-through to refunding any remaining balance back to AT creator
+
+ /* Refund balance back to AT creator */
+ labelRefund = codeByteBuffer.position();
+
+ // Set refunded mode
+ codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.REFUNDED.value));
+ // We're finished forever (finishing auto-refunds remaining balance to AT creator)
+ codeByteBuffer.put(OpCode.FIN_IMD.compile());
+ } catch (CompilationException e) {
+ throw new IllegalStateException("Unable to compile DGB-QORT ACCT?", e);
+ }
+ }
+
+ codeByteBuffer.flip();
+
+ byte[] codeBytes = new byte[codeByteBuffer.limit()];
+ codeByteBuffer.get(codeBytes);
+
+ assert Arrays.equals(Crypto.digest(codeBytes), DigibyteACCTv3.CODE_BYTES_HASH)
+ : String.format("BTCACCT.CODE_BYTES_HASH mismatch: expected %s, actual %s", HashCode.fromBytes(CODE_BYTES_HASH), HashCode.fromBytes(Crypto.digest(codeBytes)));
+
+ final short ciyamAtVersion = 2;
+ final short numCallStackPages = 0;
+ final short numUserStackPages = 0;
+ final long minActivationAmount = 0L;
+
+ return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount);
+ }
+
+ /**
+ * Returns CrossChainTradeData with useful info extracted from AT.
+ */
+ @Override
+ public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
+ ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
+ return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
+ }
+
+ /**
+ * Returns CrossChainTradeData with useful info extracted from AT.
+ */
+ @Override
+ public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
+ ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
+ return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
+ }
+
+ /**
+ * Returns CrossChainTradeData with useful info extracted from AT.
+ */
+ public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData) throws DataException {
+ byte[] addressBytes = new byte[25]; // for general use
+ String atAddress = atStateData.getATAddress();
+
+ CrossChainTradeData tradeData = new CrossChainTradeData();
+
+ tradeData.foreignBlockchain = SupportedBlockchain.DIGIBYTE.name();
+ tradeData.acctName = NAME;
+
+ tradeData.qortalAtAddress = atAddress;
+ tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
+ tradeData.creationTimestamp = creationTimestamp;
+
+ Account atAccount = new Account(repository, atAddress);
+ tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
+
+ byte[] stateData = atStateData.getStateData();
+ ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData);
+ dataByteBuffer.position(MachineState.HEADER_LENGTH);
+
+ /* Constants */
+
+ // Skip creator's trade address
+ dataByteBuffer.get(addressBytes);
+ tradeData.qortalCreatorTradeAddress = Base58.encode(addressBytes);
+ dataByteBuffer.position(dataByteBuffer.position() + 32 - addressBytes.length);
+
+ // Creator's Digibyte/foreign public key hash
+ tradeData.creatorForeignPKH = new byte[20];
+ dataByteBuffer.get(tradeData.creatorForeignPKH);
+ dataByteBuffer.position(dataByteBuffer.position() + 32 - tradeData.creatorForeignPKH.length); // skip to 32 bytes
+
+ // We don't use secret-B
+ tradeData.hashOfSecretB = null;
+
+ // Redeem payout
+ tradeData.qortAmount = dataByteBuffer.getLong();
+
+ // Expected DGB amount
+ tradeData.expectedForeignAmount = dataByteBuffer.getLong();
+
+ // Trade timeout
+ tradeData.tradeTimeout = (int) dataByteBuffer.getLong();
+
+ // Skip MESSAGE transaction type
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip expected 'trade' message length
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip expected 'redeem' message length
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip pointer to creator's address
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip pointer to partner's Qortal trade address
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip pointer to message sender
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip 'trade' message data offset for partner's Digibyte PKH
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip pointer to partner's Digibyte PKH
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip 'trade' message data offset for hash-of-secret-A
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip pointer to hash-of-secret-A
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip 'redeem' message data offset for partner's Qortal receiving address
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip pointer to message data
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip message data length
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip pointer to partner's receiving address
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ /* End of constants / begin variables */
+
+ // Skip AT creator's address
+ dataByteBuffer.position(dataByteBuffer.position() + 8 * 4);
+
+ // Partner's trade address (if present)
+ dataByteBuffer.get(addressBytes);
+ String qortalRecipient = Base58.encode(addressBytes);
+ dataByteBuffer.position(dataByteBuffer.position() + 32 - addressBytes.length);
+
+ // Potential lockTimeA (if in trade mode)
+ int lockTimeA = (int) dataByteBuffer.getLong();
+
+ // AT refund timeout (probably only useful for debugging)
+ int refundTimeout = (int) dataByteBuffer.getLong();
+
+ // Trade-mode refund timestamp (AT 'timestamp' converted to Qortal block height)
+ long tradeRefundTimestamp = dataByteBuffer.getLong();
+
+ // Skip last transaction timestamp
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip block timestamp
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip transaction type
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip temporary result
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip temporary message sender
+ dataByteBuffer.position(dataByteBuffer.position() + 8 * 4);
+
+ // Skip message length
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip temporary message data
+ dataByteBuffer.position(dataByteBuffer.position() + 8 * 4);
+
+ // Potential hash160 of secret A
+ byte[] hashOfSecretA = new byte[20];
+ dataByteBuffer.get(hashOfSecretA);
+ dataByteBuffer.position(dataByteBuffer.position() + 32 - hashOfSecretA.length); // skip to 32 bytes
+
+ // Potential partner's Digibyte PKH
+ byte[] partnerDigibytePKH = new byte[20];
+ dataByteBuffer.get(partnerDigibytePKH);
+ dataByteBuffer.position(dataByteBuffer.position() + 32 - partnerDigibytePKH.length); // skip to 32 bytes
+
+ // Partner's receiving address (if present)
+ byte[] partnerReceivingAddress = new byte[25];
+ dataByteBuffer.get(partnerReceivingAddress);
+ dataByteBuffer.position(dataByteBuffer.position() + 32 - partnerReceivingAddress.length); // skip to 32 bytes
+
+ // Trade AT's 'mode'
+ long modeValue = dataByteBuffer.getLong();
+ AcctMode mode = AcctMode.valueOf((int) (modeValue & 0xffL));
+
+ /* End of variables */
+
+ if (mode != null && mode != AcctMode.OFFERING) {
+ tradeData.mode = mode;
+ tradeData.refundTimeout = refundTimeout;
+ tradeData.tradeRefundHeight = new Timestamp(tradeRefundTimestamp).blockHeight;
+ tradeData.qortalPartnerAddress = qortalRecipient;
+ tradeData.hashOfSecretA = hashOfSecretA;
+ tradeData.partnerForeignPKH = partnerDigibytePKH;
+ tradeData.lockTimeA = lockTimeA;
+
+ if (mode == AcctMode.REDEEMED)
+ tradeData.qortalPartnerReceivingAddress = Base58.encode(partnerReceivingAddress);
+ } else {
+ tradeData.mode = AcctMode.OFFERING;
+ }
+
+ tradeData.duplicateDeprecated();
+
+ return tradeData;
+ }
+
+ /** Returns 'offer' MESSAGE payload for trade partner to send to AT creator's trade address. */
+ public static byte[] buildOfferMessage(byte[] partnerBitcoinPKH, byte[] hashOfSecretA, int lockTimeA) {
+ byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA);
+ return Bytes.concat(partnerBitcoinPKH, hashOfSecretA, lockTimeABytes);
+ }
+
+ /** Returns info extracted from 'offer' MESSAGE payload sent by trade partner to AT creator's trade address, or null if not valid. */
+ public static OfferMessageData extractOfferMessageData(byte[] messageData) {
+ if (messageData == null || messageData.length != OFFER_MESSAGE_LENGTH)
+ return null;
+
+ OfferMessageData offerMessageData = new OfferMessageData();
+ offerMessageData.partnerDigibytePKH = Arrays.copyOfRange(messageData, 0, 20);
+ offerMessageData.hashOfSecretA = Arrays.copyOfRange(messageData, 20, 40);
+ offerMessageData.lockTimeA = BitTwiddling.longFromBEBytes(messageData, 40);
+
+ return offerMessageData;
+ }
+
+ /** Returns 'trade' MESSAGE payload for AT creator to send to AT. */
+ public static byte[] buildTradeMessage(String partnerQortalTradeAddress, byte[] partnerBitcoinPKH, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
+ byte[] data = new byte[TRADE_MESSAGE_LENGTH];
+ byte[] partnerQortalAddressBytes = Base58.decode(partnerQortalTradeAddress);
+ byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA);
+ byte[] refundTimeoutBytes = BitTwiddling.toBEByteArray((long) refundTimeout);
+
+ System.arraycopy(partnerQortalAddressBytes, 0, data, 0, partnerQortalAddressBytes.length);
+ System.arraycopy(partnerBitcoinPKH, 0, data, 32, partnerBitcoinPKH.length);
+ System.arraycopy(refundTimeoutBytes, 0, data, 56, refundTimeoutBytes.length);
+ System.arraycopy(hashOfSecretA, 0, data, 64, hashOfSecretA.length);
+ System.arraycopy(lockTimeABytes, 0, data, 88, lockTimeABytes.length);
+
+ return data;
+ }
+
+ /** Returns 'cancel' MESSAGE payload for AT creator to cancel trade AT. */
+ @Override
+ public byte[] buildCancelMessage(String creatorQortalAddress) {
+ byte[] data = new byte[CANCEL_MESSAGE_LENGTH];
+ byte[] creatorQortalAddressBytes = Base58.decode(creatorQortalAddress);
+
+ System.arraycopy(creatorQortalAddressBytes, 0, data, 0, creatorQortalAddressBytes.length);
+
+ return data;
+ }
+
+ /** Returns 'redeem' MESSAGE payload for trade partner to send to AT. */
+ public static byte[] buildRedeemMessage(byte[] secretA, String qortalReceivingAddress) {
+ byte[] data = new byte[REDEEM_MESSAGE_LENGTH];
+ byte[] qortalReceivingAddressBytes = Base58.decode(qortalReceivingAddress);
+
+ System.arraycopy(secretA, 0, data, 0, secretA.length);
+ System.arraycopy(qortalReceivingAddressBytes, 0, data, 32, qortalReceivingAddressBytes.length);
+
+ return data;
+ }
+
+ /** Returns refund timeout (minutes) based on trade partner's 'offer' MESSAGE timestamp and P2SH-A locktime. */
+ public static int calcRefundTimeout(long offerMessageTimestamp, int lockTimeA) {
+ // refund should be triggered halfway between offerMessageTimestamp and lockTimeA
+ return (int) ((lockTimeA - (offerMessageTimestamp / 1000L)) / 2L / 60L);
+ }
+
+ @Override
+ public byte[] findSecretA(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException {
+ String atAddress = crossChainTradeData.qortalAtAddress;
+ String redeemerAddress = crossChainTradeData.qortalPartnerAddress;
+
+ // We don't have partner's public key so we check every message to AT
+ List messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, atAddress, null, null, null);
+ if (messageTransactionsData == null)
+ return null;
+
+ // Find 'redeem' message
+ for (MessageTransactionData messageTransactionData : messageTransactionsData) {
+ // Check message payload type/encryption
+ if (messageTransactionData.isText() || messageTransactionData.isEncrypted())
+ continue;
+
+ // Check message payload size
+ byte[] messageData = messageTransactionData.getData();
+ if (messageData.length != REDEEM_MESSAGE_LENGTH)
+ // Wrong payload length
+ continue;
+
+ // Check sender
+ if (!Crypto.toAddress(messageTransactionData.getSenderPublicKey()).equals(redeemerAddress))
+ // Wrong sender;
+ continue;
+
+ // Extract secretA
+ byte[] secretA = new byte[32];
+ System.arraycopy(messageData, 0, secretA, 0, secretA.length);
+
+ byte[] hashOfSecretA = Crypto.hash160(secretA);
+ if (!Arrays.equals(hashOfSecretA, crossChainTradeData.hashOfSecretA))
+ continue;
+
+ return secretA;
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/qortal/crosschain/Ravencoin.java b/src/main/java/org/qortal/crosschain/Ravencoin.java
new file mode 100644
index 00000000..d65c0a13
--- /dev/null
+++ b/src/main/java/org/qortal/crosschain/Ravencoin.java
@@ -0,0 +1,175 @@
+package org.qortal.crosschain;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.EnumMap;
+import java.util.Map;
+
+import org.bitcoinj.core.Coin;
+import org.bitcoinj.core.Context;
+import org.bitcoinj.core.NetworkParameters;
+import org.bitcoinj.params.RegTestParams;
+import org.bitcoinj.params.TestNet3Params;
+import org.libdohj.params.RavencoinMainNetParams;
+import org.qortal.crosschain.ElectrumX.Server;
+import org.qortal.crosschain.ElectrumX.Server.ConnectionType;
+import org.qortal.settings.Settings;
+
+public class Ravencoin extends Bitcoiny {
+
+ public static final String CURRENCY_CODE = "RVN";
+
+ private static final Coin DEFAULT_FEE_PER_KB = Coin.valueOf(1125000); // 0.01125 RVN per 1000 bytes
+
+ private static final long MINIMUM_ORDER_AMOUNT = 1000000; // 0.01 RVN minimum order, to avoid dust errors
+
+ // Temporary values until a dynamic fee system is written.
+ private static final long MAINNET_FEE = 1000000L;
+ private static final long NON_MAINNET_FEE = 1000000L; // enough for TESTNET3 and should be OK for REGTEST
+
+ private static final Map DEFAULT_ELECTRUMX_PORTS = new EnumMap<>(ConnectionType.class);
+ static {
+ DEFAULT_ELECTRUMX_PORTS.put(ConnectionType.TCP, 50001);
+ DEFAULT_ELECTRUMX_PORTS.put(ConnectionType.SSL, 50002);
+ }
+
+ public enum RavencoinNet {
+ MAIN {
+ @Override
+ public NetworkParameters getParams() {
+ return RavencoinMainNetParams.get();
+ }
+
+ @Override
+ public Collection getServers() {
+ return Arrays.asList(
+ // Servers chosen on NO BASIS WHATSOEVER from various sources!
+ // Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=rvn
+ new Server("aethyn.com", ConnectionType.SSL, 50002),
+ new Server("electrum2.rvn.rocks", ConnectionType.SSL, 50002),
+ new Server("rvn-dashboard.com", ConnectionType.SSL, 50002),
+ new Server("rvn4lyfe.com", ConnectionType.SSL, 50002),
+ new Server("electrum1.cipig.net", ConnectionType.SSL, 20051),
+ new Server("electrum2.cipig.net", ConnectionType.SSL, 20051),
+ new Server("electrum3.cipig.net", ConnectionType.SSL, 20051));
+ }
+
+ @Override
+ public String getGenesisHash() {
+ return "0000006b444bc2f2ffe627be9d9e7e7a0730000870ef6eb6da46c8eae389df90";
+ }
+
+ @Override
+ public long getP2shFee(Long timestamp) {
+ // TODO: This will need to be replaced with something better in the near future!
+ return MAINNET_FEE;
+ }
+ },
+ TEST3 {
+ @Override
+ public NetworkParameters getParams() {
+ return TestNet3Params.get();
+ }
+
+ @Override
+ public Collection getServers() {
+ return Arrays.asList(); // TODO: find testnet servers
+ }
+
+ @Override
+ public String getGenesisHash() {
+ return "000000ecfc5e6324a079542221d00e10362bdc894d56500c414060eea8a3ad5a";
+ }
+
+ @Override
+ public long getP2shFee(Long timestamp) {
+ return NON_MAINNET_FEE;
+ }
+ },
+ REGTEST {
+ @Override
+ public NetworkParameters getParams() {
+ return RegTestParams.get();
+ }
+
+ @Override
+ public Collection getServers() {
+ return Arrays.asList(
+ new Server("localhost", ConnectionType.TCP, 50001),
+ new Server("localhost", ConnectionType.SSL, 50002));
+ }
+
+ @Override
+ public String getGenesisHash() {
+ // This is unique to each regtest instance
+ return null;
+ }
+
+ @Override
+ public long getP2shFee(Long timestamp) {
+ return NON_MAINNET_FEE;
+ }
+ };
+
+ public abstract NetworkParameters getParams();
+ public abstract Collection getServers();
+ public abstract String getGenesisHash();
+ public abstract long getP2shFee(Long timestamp) throws ForeignBlockchainException;
+ }
+
+ private static Ravencoin instance;
+
+ private final RavencoinNet ravencoinNet;
+
+ // Constructors and instance
+
+ private Ravencoin(RavencoinNet ravencoinNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
+ super(blockchain, bitcoinjContext, currencyCode);
+ this.ravencoinNet = ravencoinNet;
+
+ LOGGER.info(() -> String.format("Starting Ravencoin support using %s", this.ravencoinNet.name()));
+ }
+
+ public static synchronized Ravencoin getInstance() {
+ if (instance == null) {
+ RavencoinNet ravencoinNet = Settings.getInstance().getRavencoinNet();
+
+ BitcoinyBlockchainProvider electrumX = new ElectrumX("Ravencoin-" + ravencoinNet.name(), ravencoinNet.getGenesisHash(), ravencoinNet.getServers(), DEFAULT_ELECTRUMX_PORTS);
+ Context bitcoinjContext = new Context(ravencoinNet.getParams());
+
+ instance = new Ravencoin(ravencoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
+ }
+
+ return instance;
+ }
+
+ // Getters & setters
+
+ public static synchronized void resetForTesting() {
+ instance = null;
+ }
+
+ // Actual useful methods for use by other classes
+
+ @Override
+ public Coin getFeePerKb() {
+ return DEFAULT_FEE_PER_KB;
+ }
+
+ @Override
+ public long getMinimumOrderAmount() {
+ return MINIMUM_ORDER_AMOUNT;
+ }
+
+ /**
+ * Returns estimated RVN fee, in sats per 1000bytes, optionally for historic timestamp.
+ *
+ * @param timestamp optional milliseconds since epoch, or null for 'now'
+ * @return sats per 1000bytes, or throws ForeignBlockchainException if something went wrong
+ */
+ @Override
+ public long getP2shFee(Long timestamp) throws ForeignBlockchainException {
+ return this.ravencoinNet.getP2shFee(timestamp);
+ }
+
+}
diff --git a/src/main/java/org/qortal/crosschain/RavencoinACCTv3.java b/src/main/java/org/qortal/crosschain/RavencoinACCTv3.java
new file mode 100644
index 00000000..866e2d6b
--- /dev/null
+++ b/src/main/java/org/qortal/crosschain/RavencoinACCTv3.java
@@ -0,0 +1,858 @@
+package org.qortal.crosschain;
+
+import com.google.common.hash.HashCode;
+import com.google.common.primitives.Bytes;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.ciyam.at.*;
+import org.qortal.account.Account;
+import org.qortal.asset.Asset;
+import org.qortal.at.QortalFunctionCode;
+import org.qortal.crypto.Crypto;
+import org.qortal.data.at.ATData;
+import org.qortal.data.at.ATStateData;
+import org.qortal.data.crosschain.CrossChainTradeData;
+import org.qortal.data.transaction.MessageTransactionData;
+import org.qortal.repository.DataException;
+import org.qortal.repository.Repository;
+import org.qortal.utils.Base58;
+import org.qortal.utils.BitTwiddling;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.ciyam.at.OpCode.calcOffset;
+
+/**
+ * Cross-chain trade AT
+ *
+ *
+ *
+ * - Bob generates Ravencoin & Qortal 'trade' keys
+ *
+ * - private key required to sign P2SH redeem tx
+ * - private key could be used to create 'secret' (e.g. double-SHA256)
+ * - encrypted private key could be stored in Qortal AT for access by Bob from any node
+ *
+ *
+ * - Bob deploys Qortal AT
+ *
+ *
+ * - Alice finds Qortal AT and wants to trade
+ *
+ * - Alice generates Ravencoin & Qortal 'trade' keys
+ * - Alice funds Ravencoin P2SH-A
+ * - Alice sends 'offer' MESSAGE to Bob from her Qortal trade address, containing:
+ *
+ * - hash-of-secret-A
+ * - her 'trade' Ravencoin PKH
+ *
+ *
+ *
+ *
+ * - Bob receives "offer" MESSAGE
+ *
+ * - Checks Alice's P2SH-A
+ * - Sends 'trade' MESSAGE to Qortal AT from his trade address, containing:
+ *
+ * - Alice's trade Qortal address
+ * - Alice's trade Ravencoin PKH
+ * - hash-of-secret-A
+ *
+ *
+ *
+ *
+ * - Alice checks Qortal AT to confirm it's locked to her
+ *
+ * - Alice sends 'redeem' MESSAGE to Qortal AT from her trade address, containing:
+ *
+ * - secret-A
+ * - Qortal receiving address of her chosing
+ *
+ *
+ * - AT's QORT funds are sent to Qortal receiving address
+ *
+ *
+ * - Bob checks AT, extracts secret-A
+ *
+ * - Bob redeems P2SH-A using his Ravencoin trade key and secret-A
+ * - P2SH-A RVN funds end up at Ravencoin address determined by redeem transaction output(s)
+ *
+ *
+ *
+ */
+public class RavencoinACCTv3 implements ACCT {
+
+ private static final Logger LOGGER = LogManager.getLogger(RavencoinACCTv3.class);
+
+ public static final String NAME = RavencoinACCTv3.class.getSimpleName();
+ public static final byte[] CODE_BYTES_HASH = HashCode.fromString("91395fa1ec0dfa35beddb0a7f4cc0a1bede157c38787ddb0af0cf03dfdc10f77").asBytes(); // SHA256 of AT code bytes
+
+ public static final int SECRET_LENGTH = 32;
+
+ /** Value offset into AT segment where 'mode' variable (long) is stored. (Multiply by MachineState.VALUE_SIZE for byte offset). */
+ private static final int MODE_VALUE_OFFSET = 61;
+ /** Byte offset into AT state data where 'mode' variable (long) is stored. */
+ public static final int MODE_BYTE_OFFSET = MachineState.HEADER_LENGTH + (MODE_VALUE_OFFSET * MachineState.VALUE_SIZE);
+
+ public static class OfferMessageData {
+ public byte[] partnerRavencoinPKH;
+ public byte[] hashOfSecretA;
+ public long lockTimeA;
+ }
+ public static final int OFFER_MESSAGE_LENGTH = 20 /*partnerRavencoinPKH*/ + 20 /*hashOfSecretA*/ + 8 /*lockTimeA*/;
+ public static final int TRADE_MESSAGE_LENGTH = 32 /*partner's Qortal trade address (padded from 25 to 32)*/
+ + 24 /*partner's Ravencoin PKH (padded from 20 to 24)*/
+ + 8 /*AT trade timeout (minutes)*/
+ + 24 /*hash of secret-A (padded from 20 to 24)*/
+ + 8 /*lockTimeA*/;
+ public static final int REDEEM_MESSAGE_LENGTH = 32 /*secret-A*/ + 32 /*partner's Qortal receiving address padded from 25 to 32*/;
+ public static final int CANCEL_MESSAGE_LENGTH = 32 /*AT creator's Qortal address*/;
+
+ private static RavencoinACCTv3 instance;
+
+ private RavencoinACCTv3() {
+ }
+
+ public static synchronized RavencoinACCTv3 getInstance() {
+ if (instance == null)
+ instance = new RavencoinACCTv3();
+
+ return instance;
+ }
+
+ @Override
+ public byte[] getCodeBytesHash() {
+ return CODE_BYTES_HASH;
+ }
+
+ @Override
+ public int getModeByteOffset() {
+ return MODE_BYTE_OFFSET;
+ }
+
+ @Override
+ public ForeignBlockchain getBlockchain() {
+ return Ravencoin.getInstance();
+ }
+
+ /**
+ * Returns Qortal AT creation bytes for cross-chain trading AT.
+ *
+ * tradeTimeout (minutes) is the time window for the trade partner to send the
+ * 32-byte secret to the AT, before the AT automatically refunds the AT's creator.
+ *
+ * @param creatorTradeAddress AT creator's trade Qortal address
+ * @param ravencoinPublicKeyHash 20-byte HASH160 of creator's trade Ravencoin public key
+ * @param qortAmount how much QORT to pay trade partner if they send correct 32-byte secrets to AT
+ * @param ravencoinAmount how much RVN the AT creator is expecting to trade
+ * @param tradeTimeout suggested timeout for entire trade
+ */
+ public static byte[] buildQortalAT(String creatorTradeAddress, byte[] ravencoinPublicKeyHash, long qortAmount, long ravencoinAmount, int tradeTimeout) {
+ if (ravencoinPublicKeyHash.length != 20)
+ throw new IllegalArgumentException("Ravencoin public key hash should be 20 bytes");
+
+ // Labels for data segment addresses
+ int addrCounter = 0;
+
+ // Constants (with corresponding dataByteBuffer.put*() calls below)
+
+ final int addrCreatorTradeAddress1 = addrCounter++;
+ final int addrCreatorTradeAddress2 = addrCounter++;
+ final int addrCreatorTradeAddress3 = addrCounter++;
+ final int addrCreatorTradeAddress4 = addrCounter++;
+
+ final int addrRavencoinPublicKeyHash = addrCounter;
+ addrCounter += 4;
+
+ final int addrQortAmount = addrCounter++;
+ final int addrRavencoinAmount = addrCounter++;
+ final int addrTradeTimeout = addrCounter++;
+
+ final int addrMessageTxnType = addrCounter++;
+ final int addrExpectedTradeMessageLength = addrCounter++;
+ final int addrExpectedRedeemMessageLength = addrCounter++;
+
+ final int addrCreatorAddressPointer = addrCounter++;
+ final int addrQortalPartnerAddressPointer = addrCounter++;
+ final int addrMessageSenderPointer = addrCounter++;
+
+ final int addrTradeMessagePartnerRavencoinPKHOffset = addrCounter++;
+ final int addrPartnerRavencoinPKHPointer = addrCounter++;
+ final int addrTradeMessageHashOfSecretAOffset = addrCounter++;
+ final int addrHashOfSecretAPointer = addrCounter++;
+
+ final int addrRedeemMessageReceivingAddressOffset = addrCounter++;
+
+ final int addrMessageDataPointer = addrCounter++;
+ final int addrMessageDataLength = addrCounter++;
+
+ final int addrPartnerReceivingAddressPointer = addrCounter++;
+
+ final int addrEndOfConstants = addrCounter;
+
+ // Variables
+
+ final int addrCreatorAddress1 = addrCounter++;
+ final int addrCreatorAddress2 = addrCounter++;
+ final int addrCreatorAddress3 = addrCounter++;
+ final int addrCreatorAddress4 = addrCounter++;
+
+ final int addrQortalPartnerAddress1 = addrCounter++;
+ final int addrQortalPartnerAddress2 = addrCounter++;
+ final int addrQortalPartnerAddress3 = addrCounter++;
+ final int addrQortalPartnerAddress4 = addrCounter++;
+
+ final int addrLockTimeA = addrCounter++;
+ final int addrRefundTimeout = addrCounter++;
+ final int addrRefundTimestamp = addrCounter++;
+ final int addrLastTxnTimestamp = addrCounter++;
+ final int addrBlockTimestamp = addrCounter++;
+ final int addrTxnType = addrCounter++;
+ final int addrResult = addrCounter++;
+
+ final int addrMessageSender1 = addrCounter++;
+ final int addrMessageSender2 = addrCounter++;
+ final int addrMessageSender3 = addrCounter++;
+ final int addrMessageSender4 = addrCounter++;
+
+ final int addrMessageLength = addrCounter++;
+
+ final int addrMessageData = addrCounter;
+ addrCounter += 4;
+
+ final int addrHashOfSecretA = addrCounter;
+ addrCounter += 4;
+
+ final int addrPartnerRavencoinPKH = addrCounter;
+ addrCounter += 4;
+
+ final int addrPartnerReceivingAddress = addrCounter;
+ addrCounter += 4;
+
+ final int addrMode = addrCounter++;
+ assert addrMode == MODE_VALUE_OFFSET : String.format("addrMode %d does not match MODE_VALUE_OFFSET %d", addrMode, MODE_VALUE_OFFSET);
+
+ // Data segment
+ ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
+
+ // AT creator's trade Qortal address, decoded from Base58
+ assert dataByteBuffer.position() == addrCreatorTradeAddress1 * MachineState.VALUE_SIZE : "addrCreatorTradeAddress1 incorrect";
+ byte[] creatorTradeAddressBytes = Base58.decode(creatorTradeAddress);
+ dataByteBuffer.put(Bytes.ensureCapacity(creatorTradeAddressBytes, 32, 0));
+
+ // Ravencoin public key hash
+ assert dataByteBuffer.position() == addrRavencoinPublicKeyHash * MachineState.VALUE_SIZE : "addrRavencoinPublicKeyHash incorrect";
+ dataByteBuffer.put(Bytes.ensureCapacity(ravencoinPublicKeyHash, 32, 0));
+
+ // Redeem Qort amount
+ assert dataByteBuffer.position() == addrQortAmount * MachineState.VALUE_SIZE : "addrQortAmount incorrect";
+ dataByteBuffer.putLong(qortAmount);
+
+ // Expected Ravencoin amount
+ assert dataByteBuffer.position() == addrRavencoinAmount * MachineState.VALUE_SIZE : "addrRavencoinAmount incorrect";
+ dataByteBuffer.putLong(ravencoinAmount);
+
+ // Suggested trade timeout (minutes)
+ assert dataByteBuffer.position() == addrTradeTimeout * MachineState.VALUE_SIZE : "addrTradeTimeout incorrect";
+ dataByteBuffer.putLong(tradeTimeout);
+
+ // We're only interested in MESSAGE transactions
+ assert dataByteBuffer.position() == addrMessageTxnType * MachineState.VALUE_SIZE : "addrMessageTxnType incorrect";
+ dataByteBuffer.putLong(API.ATTransactionType.MESSAGE.value);
+
+ // Expected length of 'trade' MESSAGE data from AT creator
+ assert dataByteBuffer.position() == addrExpectedTradeMessageLength * MachineState.VALUE_SIZE : "addrExpectedTradeMessageLength incorrect";
+ dataByteBuffer.putLong(TRADE_MESSAGE_LENGTH);
+
+ // Expected length of 'redeem' MESSAGE data from trade partner
+ assert dataByteBuffer.position() == addrExpectedRedeemMessageLength * MachineState.VALUE_SIZE : "addrExpectedRedeemMessageLength incorrect";
+ dataByteBuffer.putLong(REDEEM_MESSAGE_LENGTH);
+
+ // Index into data segment of AT creator's address, used by GET_B_IND
+ assert dataByteBuffer.position() == addrCreatorAddressPointer * MachineState.VALUE_SIZE : "addrCreatorAddressPointer incorrect";
+ dataByteBuffer.putLong(addrCreatorAddress1);
+
+ // Index into data segment of partner's Qortal address, used by SET_B_IND
+ assert dataByteBuffer.position() == addrQortalPartnerAddressPointer * MachineState.VALUE_SIZE : "addrQortalPartnerAddressPointer incorrect";
+ dataByteBuffer.putLong(addrQortalPartnerAddress1);
+
+ // Index into data segment of (temporary) transaction's sender's address, used by GET_B_IND
+ assert dataByteBuffer.position() == addrMessageSenderPointer * MachineState.VALUE_SIZE : "addrMessageSenderPointer incorrect";
+ dataByteBuffer.putLong(addrMessageSender1);
+
+ // Offset into 'trade' MESSAGE data payload for extracting partner's Ravencoin PKH
+ assert dataByteBuffer.position() == addrTradeMessagePartnerRavencoinPKHOffset * MachineState.VALUE_SIZE : "addrTradeMessagePartnerRavencoinPKHOffset incorrect";
+ dataByteBuffer.putLong(32L);
+
+ // Index into data segment of partner's Ravencoin PKH, used by GET_B_IND
+ assert dataByteBuffer.position() == addrPartnerRavencoinPKHPointer * MachineState.VALUE_SIZE : "addrPartnerRavencoinPKHPointer incorrect";
+ dataByteBuffer.putLong(addrPartnerRavencoinPKH);
+
+ // Offset into 'trade' MESSAGE data payload for extracting hash-of-secret-A
+ assert dataByteBuffer.position() == addrTradeMessageHashOfSecretAOffset * MachineState.VALUE_SIZE : "addrTradeMessageHashOfSecretAOffset incorrect";
+ dataByteBuffer.putLong(64L);
+
+ // Index into data segment to hash of secret A, used by GET_B_IND
+ assert dataByteBuffer.position() == addrHashOfSecretAPointer * MachineState.VALUE_SIZE : "addrHashOfSecretAPointer incorrect";
+ dataByteBuffer.putLong(addrHashOfSecretA);
+
+ // Offset into 'redeem' MESSAGE data payload for extracting Qortal receiving address
+ assert dataByteBuffer.position() == addrRedeemMessageReceivingAddressOffset * MachineState.VALUE_SIZE : "addrRedeemMessageReceivingAddressOffset incorrect";
+ dataByteBuffer.putLong(32L);
+
+ // Source location and length for hashing any passed secret
+ assert dataByteBuffer.position() == addrMessageDataPointer * MachineState.VALUE_SIZE : "addrMessageDataPointer incorrect";
+ dataByteBuffer.putLong(addrMessageData);
+ assert dataByteBuffer.position() == addrMessageDataLength * MachineState.VALUE_SIZE : "addrMessageDataLength incorrect";
+ dataByteBuffer.putLong(32L);
+
+ // Pointer into data segment of where to save partner's receiving Qortal address, used by GET_B_IND
+ assert dataByteBuffer.position() == addrPartnerReceivingAddressPointer * MachineState.VALUE_SIZE : "addrPartnerReceivingAddressPointer incorrect";
+ dataByteBuffer.putLong(addrPartnerReceivingAddress);
+
+ assert dataByteBuffer.position() == addrEndOfConstants * MachineState.VALUE_SIZE : "dataByteBuffer position not at end of constants";
+
+ // Code labels
+ Integer labelRefund = null;
+
+ Integer labelTradeTxnLoop = null;
+ Integer labelCheckTradeTxn = null;
+ Integer labelCheckCancelTxn = null;
+ Integer labelNotTradeNorCancelTxn = null;
+ Integer labelCheckNonRefundTradeTxn = null;
+ Integer labelTradeTxnExtract = null;
+ Integer labelRedeemTxnLoop = null;
+ Integer labelCheckRedeemTxn = null;
+ Integer labelCheckRedeemTxnSender = null;
+ Integer labelPayout = null;
+
+ ByteBuffer codeByteBuffer = ByteBuffer.allocate(768);
+
+ // Two-pass version
+ for (int pass = 0; pass < 2; ++pass) {
+ codeByteBuffer.clear();
+
+ try {
+ /* Initialization */
+
+ // Use AT creation 'timestamp' as starting point for finding transactions sent to AT
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrLastTxnTimestamp));
+
+ // Load B register with AT creator's address so we can save it into addrCreatorAddress1-4
+ codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_CREATOR_INTO_B));
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrCreatorAddressPointer));
+
+ /* NOP - to ensure RAVENCOIN ACCT is unique */
+ codeByteBuffer.put(OpCode.NOP.compile());
+
+ // Set restart position to after this opcode
+ codeByteBuffer.put(OpCode.SET_PCS.compile());
+
+ /* Loop, waiting for message from AT creator's trade address containing trade partner details, or AT owner's address to cancel offer */
+
+ /* Transaction processing loop */
+ labelTradeTxnLoop = codeByteBuffer.position();
+
+ /* Sleep until message arrives */
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.SLEEP_UNTIL_MESSAGE.value, addrLastTxnTimestamp));
+
+ // Find next transaction (if any) to this AT since the last one (referenced by addrLastTxnTimestamp)
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxnTimestamp));
+ // If no transaction found, A will be zero. If A is zero, set addrResult to 1, otherwise 0.
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.CHECK_A_IS_ZERO, addrResult));
+ // If addrResult is zero (i.e. A is non-zero, transaction was found) then go check transaction
+ codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelCheckTradeTxn)));
+ // Stop and wait for next block
+ codeByteBuffer.put(OpCode.STP_IMD.compile());
+
+ /* Check transaction */
+ labelCheckTradeTxn = codeByteBuffer.position();
+
+ // Update our 'last found transaction's timestamp' using 'timestamp' from transaction
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A, addrLastTxnTimestamp));
+ // Extract transaction type (message/payment) from transaction and save type in addrTxnType
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TYPE_FROM_TX_IN_A, addrTxnType));
+ // If transaction type is not MESSAGE type then go look for another transaction
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxnType, addrMessageTxnType, calcOffset(codeByteBuffer, labelTradeTxnLoop)));
+
+ /* Check transaction's sender. We're expecting AT creator's trade address for 'trade' message, or AT creator's own address for 'cancel' message. */
+
+ // Extract sender address from transaction into B register
+ codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_ADDRESS_FROM_TX_IN_A_INTO_B));
+ // Save B register into data segment starting at addrMessageSender1 (as pointed to by addrMessageSenderPointer)
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageSenderPointer));
+ // Compare each part of message sender's address with AT creator's trade address. If they don't match, check for cancel situation.
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrCreatorTradeAddress1, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrCreatorTradeAddress2, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrCreatorTradeAddress3, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrCreatorTradeAddress4, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
+ // Message sender's address matches AT creator's trade address so go process 'trade' message
+ codeByteBuffer.put(OpCode.JMP_ADR.compile(labelCheckNonRefundTradeTxn == null ? 0 : labelCheckNonRefundTradeTxn));
+
+ /* Checking message sender for possible cancel message */
+ labelCheckCancelTxn = codeByteBuffer.position();
+
+ // Compare each part of message sender's address with AT creator's address. If they don't match, look for another transaction.
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrCreatorAddress1, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrCreatorAddress2, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrCreatorAddress3, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrCreatorAddress4, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
+ // Partner address is AT creator's address, so cancel offer and finish.
+ codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.CANCELLED.value));
+ // We're finished forever (finishing auto-refunds remaining balance to AT creator)
+ codeByteBuffer.put(OpCode.FIN_IMD.compile());
+
+ /* Not trade nor cancel message */
+ labelNotTradeNorCancelTxn = codeByteBuffer.position();
+
+ // Loop to find another transaction
+ codeByteBuffer.put(OpCode.JMP_ADR.compile(labelTradeTxnLoop == null ? 0 : labelTradeTxnLoop));
+
+ /* Possible switch-to-trade-mode message */
+ labelCheckNonRefundTradeTxn = codeByteBuffer.position();
+
+ // Check 'trade' message we received has expected number of message bytes
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(QortalFunctionCode.GET_MESSAGE_LENGTH_FROM_TX_IN_A.value, addrMessageLength));
+ // If message length matches, branch to info extraction code
+ codeByteBuffer.put(OpCode.BEQ_DAT.compile(addrMessageLength, addrExpectedTradeMessageLength, calcOffset(codeByteBuffer, labelTradeTxnExtract)));
+ // Message length didn't match - go back to finding another 'trade' MESSAGE transaction
+ codeByteBuffer.put(OpCode.JMP_ADR.compile(labelTradeTxnLoop == null ? 0 : labelTradeTxnLoop));
+
+ /* Extracting info from 'trade' MESSAGE transaction */
+ labelTradeTxnExtract = codeByteBuffer.position();
+
+ // Extract message from transaction into B register
+ codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B));
+ // Save B register into data segment starting at addrQortalPartnerAddress1 (as pointed to by addrQortalPartnerAddressPointer)
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrQortalPartnerAddressPointer));
+
+ // Extract trade partner's Ravencoin public key hash (PKH) from message into B
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessagePartnerRavencoinPKHOffset));
+ // Store partner's Ravencoin PKH (we only really use values from B1-B3)
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerRavencoinPKHPointer));
+ // Extract AT trade timeout (minutes) (from B4)
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B4, addrRefundTimeout));
+
+ // Grab next 32 bytes
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessageHashOfSecretAOffset));
+
+ // Extract hash-of-secret-A (we only really use values from B1-B3)
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrHashOfSecretAPointer));
+ // Extract lockTime-A (from B4)
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B4, addrLockTimeA));
+
+ // Calculate trade timeout refund 'timestamp' by adding addrRefundTimeout minutes to this transaction's 'timestamp', then save into addrRefundTimestamp
+ codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.ADD_MINUTES_TO_TIMESTAMP, addrRefundTimestamp, addrLastTxnTimestamp, addrRefundTimeout));
+
+ /* We are in 'trade mode' */
+ codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.TRADING.value));
+
+ // Set restart position to after this opcode
+ codeByteBuffer.put(OpCode.SET_PCS.compile());
+
+ /* Loop, waiting for trade timeout or 'redeem' MESSAGE from Qortal trade partner */
+
+ // Fetch current block 'timestamp'
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_BLOCK_TIMESTAMP, addrBlockTimestamp));
+ // If we're not past refund 'timestamp' then look for next transaction
+ codeByteBuffer.put(OpCode.BLT_DAT.compile(addrBlockTimestamp, addrRefundTimestamp, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
+ // We're past refund 'timestamp' so go refund everything back to AT creator
+ codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRefund == null ? 0 : labelRefund));
+
+ /* Transaction processing loop */
+ labelRedeemTxnLoop = codeByteBuffer.position();
+
+ // Find next transaction to this AT since the last one (if any)
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxnTimestamp));
+ // If no transaction found, A will be zero. If A is zero, set addrComparator to 1, otherwise 0.
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.CHECK_A_IS_ZERO, addrResult));
+ // If addrResult is zero (i.e. A is non-zero, transaction was found) then go check transaction
+ codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelCheckRedeemTxn)));
+ // Stop and wait for next block
+ codeByteBuffer.put(OpCode.STP_IMD.compile());
+
+ /* Check transaction */
+ labelCheckRedeemTxn = codeByteBuffer.position();
+
+ // Update our 'last found transaction's timestamp' using 'timestamp' from transaction
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A, addrLastTxnTimestamp));
+ // Extract transaction type (message/payment) from transaction and save type in addrTxnType
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TYPE_FROM_TX_IN_A, addrTxnType));
+ // If transaction type is not MESSAGE type then go look for another transaction
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxnType, addrMessageTxnType, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
+
+ /* Check message payload length */
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(QortalFunctionCode.GET_MESSAGE_LENGTH_FROM_TX_IN_A.value, addrMessageLength));
+ // If message length matches, branch to sender checking code
+ codeByteBuffer.put(OpCode.BEQ_DAT.compile(addrMessageLength, addrExpectedRedeemMessageLength, calcOffset(codeByteBuffer, labelCheckRedeemTxnSender)));
+ // Message length didn't match - go back to finding another 'redeem' MESSAGE transaction
+ codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRedeemTxnLoop == null ? 0 : labelRedeemTxnLoop));
+
+ /* Check transaction's sender */
+ labelCheckRedeemTxnSender = codeByteBuffer.position();
+
+ // Extract sender address from transaction into B register
+ codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_ADDRESS_FROM_TX_IN_A_INTO_B));
+ // Save B register into data segment starting at addrMessageSender1 (as pointed to by addrMessageSenderPointer)
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageSenderPointer));
+ // Compare each part of transaction's sender's address with expected address. If they don't match, look for another transaction.
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrQortalPartnerAddress1, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrQortalPartnerAddress2, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrQortalPartnerAddress3, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrQortalPartnerAddress4, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
+
+ /* Check 'secret-A' in transaction's message */
+
+ // Extract secret-A from first 32 bytes of message from transaction into B register
+ codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B));
+ // Save B register into data segment starting at addrMessageData (as pointed to by addrMessageDataPointer)
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageDataPointer));
+ // Load B register with expected hash result (as pointed to by addrHashOfSecretAPointer)
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrHashOfSecretAPointer));
+ // Perform HASH160 using source data at addrMessageData. (Location and length specified via addrMessageDataPointer and addrMessageDataLength).
+ // Save the equality result (1 if they match, 0 otherwise) into addrResult.
+ codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.CHECK_HASH160_WITH_B, addrResult, addrMessageDataPointer, addrMessageDataLength));
+ // If hashes don't match, addrResult will be zero so go find another transaction
+ codeByteBuffer.put(OpCode.BNZ_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelPayout)));
+ codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRedeemTxnLoop == null ? 0 : labelRedeemTxnLoop));
+
+ /* Success! Pay arranged amount to receiving address */
+ labelPayout = codeByteBuffer.position();
+
+ // Extract Qortal receiving address from next 32 bytes of message from transaction into B register
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrRedeemMessageReceivingAddressOffset));
+ // Save B register into data segment starting at addrPartnerReceivingAddress (as pointed to by addrPartnerReceivingAddressPointer)
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerReceivingAddressPointer));
+ // Pay AT's balance to receiving address
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PAY_TO_ADDRESS_IN_B, addrQortAmount));
+ // Set redeemed mode
+ codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.REDEEMED.value));
+ // We're finished forever (finishing auto-refunds remaining balance to AT creator)
+ codeByteBuffer.put(OpCode.FIN_IMD.compile());
+
+ // Fall-through to refunding any remaining balance back to AT creator
+
+ /* Refund balance back to AT creator */
+ labelRefund = codeByteBuffer.position();
+
+ // Set refunded mode
+ codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.REFUNDED.value));
+ // We're finished forever (finishing auto-refunds remaining balance to AT creator)
+ codeByteBuffer.put(OpCode.FIN_IMD.compile());
+ } catch (CompilationException e) {
+ throw new IllegalStateException("Unable to compile RVN-QORT ACCT?", e);
+ }
+ }
+
+ codeByteBuffer.flip();
+
+ byte[] codeBytes = new byte[codeByteBuffer.limit()];
+ codeByteBuffer.get(codeBytes);
+
+ assert Arrays.equals(Crypto.digest(codeBytes), RavencoinACCTv3.CODE_BYTES_HASH)
+ : String.format("BTCACCT.CODE_BYTES_HASH mismatch: expected %s, actual %s", HashCode.fromBytes(CODE_BYTES_HASH), HashCode.fromBytes(Crypto.digest(codeBytes)));
+
+ final short ciyamAtVersion = 2;
+ final short numCallStackPages = 0;
+ final short numUserStackPages = 0;
+ final long minActivationAmount = 0L;
+
+ return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount);
+ }
+
+ /**
+ * Returns CrossChainTradeData with useful info extracted from AT.
+ */
+ @Override
+ public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
+ ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
+ return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
+ }
+
+ /**
+ * Returns CrossChainTradeData with useful info extracted from AT.
+ */
+ @Override
+ public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
+ ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
+ return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
+ }
+
+ /**
+ * Returns CrossChainTradeData with useful info extracted from AT.
+ */
+ public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData) throws DataException {
+ byte[] addressBytes = new byte[25]; // for general use
+ String atAddress = atStateData.getATAddress();
+
+ CrossChainTradeData tradeData = new CrossChainTradeData();
+
+ tradeData.foreignBlockchain = SupportedBlockchain.RAVENCOIN.name();
+ tradeData.acctName = NAME;
+
+ tradeData.qortalAtAddress = atAddress;
+ tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
+ tradeData.creationTimestamp = creationTimestamp;
+
+ Account atAccount = new Account(repository, atAddress);
+ tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
+
+ byte[] stateData = atStateData.getStateData();
+ ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData);
+ dataByteBuffer.position(MachineState.HEADER_LENGTH);
+
+ /* Constants */
+
+ // Skip creator's trade address
+ dataByteBuffer.get(addressBytes);
+ tradeData.qortalCreatorTradeAddress = Base58.encode(addressBytes);
+ dataByteBuffer.position(dataByteBuffer.position() + 32 - addressBytes.length);
+
+ // Creator's Ravencoin/foreign public key hash
+ tradeData.creatorForeignPKH = new byte[20];
+ dataByteBuffer.get(tradeData.creatorForeignPKH);
+ dataByteBuffer.position(dataByteBuffer.position() + 32 - tradeData.creatorForeignPKH.length); // skip to 32 bytes
+
+ // We don't use secret-B
+ tradeData.hashOfSecretB = null;
+
+ // Redeem payout
+ tradeData.qortAmount = dataByteBuffer.getLong();
+
+ // Expected RVN amount
+ tradeData.expectedForeignAmount = dataByteBuffer.getLong();
+
+ // Trade timeout
+ tradeData.tradeTimeout = (int) dataByteBuffer.getLong();
+
+ // Skip MESSAGE transaction type
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip expected 'trade' message length
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip expected 'redeem' message length
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip pointer to creator's address
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip pointer to partner's Qortal trade address
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip pointer to message sender
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip 'trade' message data offset for partner's Ravencoin PKH
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip pointer to partner's Ravencoin PKH
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip 'trade' message data offset for hash-of-secret-A
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip pointer to hash-of-secret-A
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip 'redeem' message data offset for partner's Qortal receiving address
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip pointer to message data
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip message data length
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip pointer to partner's receiving address
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ /* End of constants / begin variables */
+
+ // Skip AT creator's address
+ dataByteBuffer.position(dataByteBuffer.position() + 8 * 4);
+
+ // Partner's trade address (if present)
+ dataByteBuffer.get(addressBytes);
+ String qortalRecipient = Base58.encode(addressBytes);
+ dataByteBuffer.position(dataByteBuffer.position() + 32 - addressBytes.length);
+
+ // Potential lockTimeA (if in trade mode)
+ int lockTimeA = (int) dataByteBuffer.getLong();
+
+ // AT refund timeout (probably only useful for debugging)
+ int refundTimeout = (int) dataByteBuffer.getLong();
+
+ // Trade-mode refund timestamp (AT 'timestamp' converted to Qortal block height)
+ long tradeRefundTimestamp = dataByteBuffer.getLong();
+
+ // Skip last transaction timestamp
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip block timestamp
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip transaction type
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip temporary result
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip temporary message sender
+ dataByteBuffer.position(dataByteBuffer.position() + 8 * 4);
+
+ // Skip message length
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip temporary message data
+ dataByteBuffer.position(dataByteBuffer.position() + 8 * 4);
+
+ // Potential hash160 of secret A
+ byte[] hashOfSecretA = new byte[20];
+ dataByteBuffer.get(hashOfSecretA);
+ dataByteBuffer.position(dataByteBuffer.position() + 32 - hashOfSecretA.length); // skip to 32 bytes
+
+ // Potential partner's Ravencoin PKH
+ byte[] partnerRavencoinPKH = new byte[20];
+ dataByteBuffer.get(partnerRavencoinPKH);
+ dataByteBuffer.position(dataByteBuffer.position() + 32 - partnerRavencoinPKH.length); // skip to 32 bytes
+
+ // Partner's receiving address (if present)
+ byte[] partnerReceivingAddress = new byte[25];
+ dataByteBuffer.get(partnerReceivingAddress);
+ dataByteBuffer.position(dataByteBuffer.position() + 32 - partnerReceivingAddress.length); // skip to 32 bytes
+
+ // Trade AT's 'mode'
+ long modeValue = dataByteBuffer.getLong();
+ AcctMode mode = AcctMode.valueOf((int) (modeValue & 0xffL));
+
+ /* End of variables */
+
+ if (mode != null && mode != AcctMode.OFFERING) {
+ tradeData.mode = mode;
+ tradeData.refundTimeout = refundTimeout;
+ tradeData.tradeRefundHeight = new Timestamp(tradeRefundTimestamp).blockHeight;
+ tradeData.qortalPartnerAddress = qortalRecipient;
+ tradeData.hashOfSecretA = hashOfSecretA;
+ tradeData.partnerForeignPKH = partnerRavencoinPKH;
+ tradeData.lockTimeA = lockTimeA;
+
+ if (mode == AcctMode.REDEEMED)
+ tradeData.qortalPartnerReceivingAddress = Base58.encode(partnerReceivingAddress);
+ } else {
+ tradeData.mode = AcctMode.OFFERING;
+ }
+
+ tradeData.duplicateDeprecated();
+
+ return tradeData;
+ }
+
+ /** Returns 'offer' MESSAGE payload for trade partner to send to AT creator's trade address. */
+ public static byte[] buildOfferMessage(byte[] partnerBitcoinPKH, byte[] hashOfSecretA, int lockTimeA) {
+ byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA);
+ return Bytes.concat(partnerBitcoinPKH, hashOfSecretA, lockTimeABytes);
+ }
+
+ /** Returns info extracted from 'offer' MESSAGE payload sent by trade partner to AT creator's trade address, or null if not valid. */
+ public static OfferMessageData extractOfferMessageData(byte[] messageData) {
+ if (messageData == null || messageData.length != OFFER_MESSAGE_LENGTH)
+ return null;
+
+ OfferMessageData offerMessageData = new OfferMessageData();
+ offerMessageData.partnerRavencoinPKH = Arrays.copyOfRange(messageData, 0, 20);
+ offerMessageData.hashOfSecretA = Arrays.copyOfRange(messageData, 20, 40);
+ offerMessageData.lockTimeA = BitTwiddling.longFromBEBytes(messageData, 40);
+
+ return offerMessageData;
+ }
+
+ /** Returns 'trade' MESSAGE payload for AT creator to send to AT. */
+ public static byte[] buildTradeMessage(String partnerQortalTradeAddress, byte[] partnerBitcoinPKH, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
+ byte[] data = new byte[TRADE_MESSAGE_LENGTH];
+ byte[] partnerQortalAddressBytes = Base58.decode(partnerQortalTradeAddress);
+ byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA);
+ byte[] refundTimeoutBytes = BitTwiddling.toBEByteArray((long) refundTimeout);
+
+ System.arraycopy(partnerQortalAddressBytes, 0, data, 0, partnerQortalAddressBytes.length);
+ System.arraycopy(partnerBitcoinPKH, 0, data, 32, partnerBitcoinPKH.length);
+ System.arraycopy(refundTimeoutBytes, 0, data, 56, refundTimeoutBytes.length);
+ System.arraycopy(hashOfSecretA, 0, data, 64, hashOfSecretA.length);
+ System.arraycopy(lockTimeABytes, 0, data, 88, lockTimeABytes.length);
+
+ return data;
+ }
+
+ /** Returns 'cancel' MESSAGE payload for AT creator to cancel trade AT. */
+ @Override
+ public byte[] buildCancelMessage(String creatorQortalAddress) {
+ byte[] data = new byte[CANCEL_MESSAGE_LENGTH];
+ byte[] creatorQortalAddressBytes = Base58.decode(creatorQortalAddress);
+
+ System.arraycopy(creatorQortalAddressBytes, 0, data, 0, creatorQortalAddressBytes.length);
+
+ return data;
+ }
+
+ /** Returns 'redeem' MESSAGE payload for trade partner to send to AT. */
+ public static byte[] buildRedeemMessage(byte[] secretA, String qortalReceivingAddress) {
+ byte[] data = new byte[REDEEM_MESSAGE_LENGTH];
+ byte[] qortalReceivingAddressBytes = Base58.decode(qortalReceivingAddress);
+
+ System.arraycopy(secretA, 0, data, 0, secretA.length);
+ System.arraycopy(qortalReceivingAddressBytes, 0, data, 32, qortalReceivingAddressBytes.length);
+
+ return data;
+ }
+
+ /** Returns refund timeout (minutes) based on trade partner's 'offer' MESSAGE timestamp and P2SH-A locktime. */
+ public static int calcRefundTimeout(long offerMessageTimestamp, int lockTimeA) {
+ // refund should be triggered halfway between offerMessageTimestamp and lockTimeA
+ return (int) ((lockTimeA - (offerMessageTimestamp / 1000L)) / 2L / 60L);
+ }
+
+ @Override
+ public byte[] findSecretA(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException {
+ String atAddress = crossChainTradeData.qortalAtAddress;
+ String redeemerAddress = crossChainTradeData.qortalPartnerAddress;
+
+ // We don't have partner's public key so we check every message to AT
+ List messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, atAddress, null, null, null);
+ if (messageTransactionsData == null)
+ return null;
+
+ // Find 'redeem' message
+ for (MessageTransactionData messageTransactionData : messageTransactionsData) {
+ // Check message payload type/encryption
+ if (messageTransactionData.isText() || messageTransactionData.isEncrypted())
+ continue;
+
+ // Check message payload size
+ byte[] messageData = messageTransactionData.getData();
+ if (messageData.length != REDEEM_MESSAGE_LENGTH)
+ // Wrong payload length
+ continue;
+
+ // Check sender
+ if (!Crypto.toAddress(messageTransactionData.getSenderPublicKey()).equals(redeemerAddress))
+ // Wrong sender;
+ continue;
+
+ // Extract secretA
+ byte[] secretA = new byte[32];
+ System.arraycopy(messageData, 0, secretA, 0, secretA.length);
+
+ byte[] hashOfSecretA = Crypto.hash160(secretA);
+ if (!Arrays.equals(hashOfSecretA, crossChainTradeData.hashOfSecretA))
+ continue;
+
+ return secretA;
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/qortal/crosschain/SupportedBlockchain.java b/src/main/java/org/qortal/crosschain/SupportedBlockchain.java
index a26e0e01..5e3b4078 100644
--- a/src/main/java/org/qortal/crosschain/SupportedBlockchain.java
+++ b/src/main/java/org/qortal/crosschain/SupportedBlockchain.java
@@ -57,6 +57,34 @@ public enum SupportedBlockchain {
public ACCT getLatestAcct() {
return DogecoinACCTv3.getInstance();
}
+ },
+
+ DIGIBYTE(Arrays.asList(
+ Triple.valueOf(DigibyteACCTv3.NAME, DigibyteACCTv3.CODE_BYTES_HASH, DigibyteACCTv3::getInstance)
+ )) {
+ @Override
+ public ForeignBlockchain getInstance() {
+ return Digibyte.getInstance();
+ }
+
+ @Override
+ public ACCT getLatestAcct() {
+ return DigibyteACCTv3.getInstance();
+ }
+ },
+
+ RAVENCOIN(Arrays.asList(
+ Triple.valueOf(RavencoinACCTv3.NAME, RavencoinACCTv3.CODE_BYTES_HASH, RavencoinACCTv3::getInstance)
+ )) {
+ @Override
+ public ForeignBlockchain getInstance() {
+ return Ravencoin.getInstance();
+ }
+
+ @Override
+ public ACCT getLatestAcct() {
+ return RavencoinACCTv3.getInstance();
+ }
};
private static final Map> supportedAcctsByCodeHash = Arrays.stream(SupportedBlockchain.values())
diff --git a/src/main/java/org/qortal/gui/Gui.java b/src/main/java/org/qortal/gui/Gui.java
index 87342f6a..4944db52 100644
--- a/src/main/java/org/qortal/gui/Gui.java
+++ b/src/main/java/org/qortal/gui/Gui.java
@@ -4,6 +4,7 @@ import java.awt.GraphicsEnvironment;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
+import java.util.ServiceConfigurationError;
import javax.imageio.ImageIO;
import javax.swing.JOptionPane;
@@ -49,7 +50,7 @@ public class Gui {
protected static BufferedImage loadImage(String resourceName) {
try (InputStream in = Gui.class.getResourceAsStream("/images/" + resourceName)) {
return ImageIO.read(in);
- } catch (IllegalArgumentException | IOException e) {
+ } catch (IllegalArgumentException | IOException | ServiceConfigurationError e) {
LOGGER.warn(String.format("Couldn't locate image resource \"images/%s\"", resourceName));
return null;
}
diff --git a/src/main/java/org/qortal/network/Handshake.java b/src/main/java/org/qortal/network/Handshake.java
index cdcff1d7..22354cc4 100644
--- a/src/main/java/org/qortal/network/Handshake.java
+++ b/src/main/java/org/qortal/network/Handshake.java
@@ -13,7 +13,7 @@ import org.qortal.crypto.MemoryPoW;
import org.qortal.network.message.ChallengeMessage;
import org.qortal.network.message.HelloMessage;
import org.qortal.network.message.Message;
-import org.qortal.network.message.Message.MessageType;
+import org.qortal.network.message.MessageType;
import org.qortal.settings.Settings;
import org.qortal.network.message.ResponseMessage;
import org.qortal.utils.DaemonThreadFactory;
diff --git a/src/main/java/org/qortal/network/Network.java b/src/main/java/org/qortal/network/Network.java
index d4435ddb..a04509f1 100644
--- a/src/main/java/org/qortal/network/Network.java
+++ b/src/main/java/org/qortal/network/Network.java
@@ -13,6 +13,7 @@ import org.qortal.data.block.BlockData;
import org.qortal.data.network.PeerData;
import org.qortal.data.transaction.TransactionData;
import org.qortal.network.message.*;
+import org.qortal.network.task.*;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
@@ -32,6 +33,7 @@ import java.nio.channels.*;
import java.security.SecureRandom;
import java.util.*;
import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
@@ -41,9 +43,8 @@ import java.util.stream.Collectors;
// For managing peers
public class Network {
private static final Logger LOGGER = LogManager.getLogger(Network.class);
- private static Network instance;
- private static final int LISTEN_BACKLOG = 10;
+ private static final int LISTEN_BACKLOG = 5;
/**
* How long before retrying after a connection failure, in milliseconds.
*/
@@ -122,14 +123,8 @@ public class Network {
private final ExecuteProduceConsume networkEPC;
private Selector channelSelector;
private ServerSocketChannel serverChannel;
- private Iterator channelIterator = null;
-
- // volatile because value is updated inside any one of the EPC threads
- private volatile long nextConnectTaskTimestamp = 0L; // ms - try first connect once NTP syncs
-
- private final ExecutorService broadcastExecutor = Executors.newCachedThreadPool();
- // volatile because value is updated inside any one of the EPC threads
- private volatile long nextBroadcastTimestamp = 0L; // ms - try first broadcast once NTP syncs
+ private SelectionKey serverSelectionKey;
+ private final Set channelsPendingWrite = ConcurrentHashMap.newKeySet();
private final Lock mergePeersLock = new ReentrantLock();
@@ -137,6 +132,8 @@ public class Network {
private String ourExternalIpAddress = null;
private int ourExternalPort = Settings.getInstance().getListenPort();
+ private volatile boolean isShuttingDown = false;
+
// Constructors
private Network() {
@@ -170,7 +167,7 @@ public class Network {
serverChannel.configureBlocking(false);
serverChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);
serverChannel.bind(endpoint, LISTEN_BACKLOG);
- serverChannel.register(channelSelector, SelectionKey.OP_ACCEPT);
+ serverSelectionKey = serverChannel.register(channelSelector, SelectionKey.OP_ACCEPT);
} catch (UnknownHostException e) {
LOGGER.error("Can't bind listen socket to address {}", Settings.getInstance().getBindAddress());
throw new IOException("Can't bind listen socket to address", e);
@@ -180,7 +177,8 @@ public class Network {
}
// Load all known peers from repository
- synchronized (this.allKnownPeers) { List fixedNetwork = Settings.getInstance().getFixedNetwork();
+ synchronized (this.allKnownPeers) {
+ List fixedNetwork = Settings.getInstance().getFixedNetwork();
if (fixedNetwork != null && !fixedNetwork.isEmpty()) {
Long addedWhen = NTP.getTime();
String addedBy = "fixedNetwork";
@@ -214,12 +212,16 @@ public class Network {
// Getters / setters
- public static synchronized Network getInstance() {
- if (instance == null) {
- instance = new Network();
- }
+ private static class SingletonContainer {
+ private static final Network INSTANCE = new Network();
+ }
- return instance;
+ public static Network getInstance() {
+ return SingletonContainer.INSTANCE;
+ }
+
+ public int getMaxPeers() {
+ return this.maxPeers;
}
public byte[] getMessageMagic() {
@@ -453,6 +455,11 @@ public class Network {
class NetworkProcessor extends ExecuteProduceConsume {
+ private final AtomicLong nextConnectTaskTimestamp = new AtomicLong(0L); // ms - try first connect once NTP syncs
+ private final AtomicLong nextBroadcastTimestamp = new AtomicLong(0L); // ms - try first broadcast once NTP syncs
+
+ private Iterator channelIterator = null;
+
NetworkProcessor(ExecutorService executor) {
super(executor);
}
@@ -494,43 +501,23 @@ public class Network {
}
private Task maybeProducePeerMessageTask() {
- for (Peer peer : getImmutableConnectedPeers()) {
- Task peerTask = peer.getMessageTask();
- if (peerTask != null) {
- return peerTask;
- }
- }
-
- return null;
+ return getImmutableConnectedPeers().stream()
+ .map(Peer::getMessageTask)
+ .filter(Objects::nonNull)
+ .findFirst()
+ .orElse(null);
}
private Task maybeProducePeerPingTask(Long now) {
- // Ask connected peers whether they need a ping
- for (Peer peer : getImmutableHandshakedPeers()) {
- Task peerTask = peer.getPingTask(now);
- if (peerTask != null) {
- return peerTask;
- }
- }
-
- return null;
- }
-
- class PeerConnectTask implements ExecuteProduceConsume.Task {
- private final Peer peer;
-
- PeerConnectTask(Peer peer) {
- this.peer = peer;
- }
-
- @Override
- public void perform() throws InterruptedException {
- connectPeer(peer);
- }
+ return getImmutableHandshakedPeers().stream()
+ .map(peer -> peer.getPingTask(now))
+ .filter(Objects::nonNull)
+ .findFirst()
+ .orElse(null);
}
private Task maybeProduceConnectPeerTask(Long now) throws InterruptedException {
- if (now == null || now < nextConnectTaskTimestamp) {
+ if (now == null || now < nextConnectTaskTimestamp.get()) {
return null;
}
@@ -538,7 +525,7 @@ public class Network {
return null;
}
- nextConnectTaskTimestamp = now + 1000L;
+ nextConnectTaskTimestamp.set(now + 1000L);
Peer targetPeer = getConnectablePeer(now);
if (targetPeer == null) {
@@ -550,66 +537,15 @@ public class Network {
}
private Task maybeProduceBroadcastTask(Long now) {
- if (now == null || now < nextBroadcastTimestamp) {
+ if (now == null || now < nextBroadcastTimestamp.get()) {
return null;
}
- nextBroadcastTimestamp = now + BROADCAST_INTERVAL;
- return () -> Controller.getInstance().doNetworkBroadcast();
- }
-
- class ChannelTask implements ExecuteProduceConsume.Task {
- private final SelectionKey selectionKey;
-
- ChannelTask(SelectionKey selectionKey) {
- this.selectionKey = selectionKey;
- }
-
- @Override
- public void perform() throws InterruptedException {
- try {
- LOGGER.trace("Thread {} has pending channel: {}, with ops {}",
- Thread.currentThread().getId(), selectionKey.channel(), selectionKey.readyOps());
-
- // process pending channel task
- if (selectionKey.isReadable()) {
- connectionRead((SocketChannel) selectionKey.channel());
- } else if (selectionKey.isAcceptable()) {
- acceptConnection((ServerSocketChannel) selectionKey.channel());
- }
-
- LOGGER.trace("Thread {} processed channel: {}",
- Thread.currentThread().getId(), selectionKey.channel());
- } catch (CancelledKeyException e) {
- LOGGER.trace("Thread {} encountered cancelled channel: {}",
- Thread.currentThread().getId(), selectionKey.channel());
- }
- }
-
- private void connectionRead(SocketChannel socketChannel) {
- Peer peer = getPeerFromChannel(socketChannel);
- if (peer == null) {
- return;
- }
-
- try {
- peer.readChannel();
- } catch (IOException e) {
- if (e.getMessage() != null && e.getMessage().toLowerCase().contains("connection reset")) {
- peer.disconnect("Connection reset");
- return;
- }
-
- LOGGER.trace("[{}] Network thread {} encountered I/O error: {}", peer.getPeerConnectionId(),
- Thread.currentThread().getId(), e.getMessage(), e);
- peer.disconnect("I/O error");
- }
- }
+ nextBroadcastTimestamp.set(now + BROADCAST_INTERVAL);
+ return new BroadcastTask();
}
private Task maybeProduceChannelTask(boolean canBlock) throws InterruptedException {
- final SelectionKey nextSelectionKey;
-
// Synchronization here to enforce thread-safety on channelIterator
synchronized (channelSelector) {
// anything to do?
@@ -630,91 +566,73 @@ public class Network {
}
channelIterator = channelSelector.selectedKeys().iterator();
+ LOGGER.trace("Thread {}, after {} select, channelIterator now {}",
+ Thread.currentThread().getId(),
+ canBlock ? "blocking": "non-blocking",
+ channelIterator);
}
- if (channelIterator.hasNext()) {
- nextSelectionKey = channelIterator.next();
- channelIterator.remove();
- } else {
- nextSelectionKey = null;
+ if (!channelIterator.hasNext()) {
channelIterator = null; // Nothing to do so reset iterator to cause new select
+
+ LOGGER.trace("Thread {}, channelIterator now null", Thread.currentThread().getId());
+ return null;
}
- LOGGER.trace("Thread {}, nextSelectionKey {}, channelIterator now {}",
- Thread.currentThread().getId(), nextSelectionKey, channelIterator);
- }
+ final SelectionKey nextSelectionKey = channelIterator.next();
+ channelIterator.remove();
- if (nextSelectionKey == null) {
- return null;
- }
+ // Just in case underlying socket channel already closed elsewhere, etc.
+ if (!nextSelectionKey.isValid())
+ return null;
- return new ChannelTask(nextSelectionKey);
- }
- }
+ LOGGER.trace("Thread {}, nextSelectionKey {}", Thread.currentThread().getId(), nextSelectionKey);
- private void acceptConnection(ServerSocketChannel serverSocketChannel) throws InterruptedException {
- SocketChannel socketChannel;
+ SelectableChannel socketChannel = nextSelectionKey.channel();
- try {
- socketChannel = serverSocketChannel.accept();
- } catch (IOException e) {
- return;
- }
-
- // No connection actually accepted?
- if (socketChannel == null) {
- return;
- }
- PeerAddress address = PeerAddress.fromSocket(socketChannel.socket());
- List fixedNetwork = Settings.getInstance().getFixedNetwork();
- if (fixedNetwork != null && !fixedNetwork.isEmpty() && ipNotInFixedList(address, fixedNetwork)) {
- try {
- LOGGER.debug("Connection discarded from peer {} as not in the fixed network list", address);
- socketChannel.close();
- } catch (IOException e) {
- // IGNORE
- }
- return;
- }
-
- final Long now = NTP.getTime();
- Peer newPeer;
-
- try {
- if (now == null) {
- LOGGER.debug("Connection discarded from peer {} due to lack of NTP sync", address);
- socketChannel.close();
- return;
- }
-
- if (getImmutableConnectedPeers().size() >= maxPeers) {
- // We have enough peers
- LOGGER.debug("Connection discarded from peer {} because the server is full", address);
- socketChannel.close();
- return;
- }
-
- LOGGER.debug("Connection accepted from peer {}", address);
-
- newPeer = new Peer(socketChannel, channelSelector);
- this.addConnectedPeer(newPeer);
-
- } catch (IOException e) {
- if (socketChannel.isOpen()) {
try {
- LOGGER.debug("Connection failed from peer {} while connecting/closing", address);
- socketChannel.close();
- } catch (IOException ce) {
- // Couldn't close?
+ if (nextSelectionKey.isReadable()) {
+ clearInterestOps(nextSelectionKey, SelectionKey.OP_READ);
+ Peer peer = getPeerFromChannel((SocketChannel) socketChannel);
+ if (peer == null)
+ return null;
+
+ return new ChannelReadTask((SocketChannel) socketChannel, peer);
+ }
+
+ if (nextSelectionKey.isWritable()) {
+ clearInterestOps(nextSelectionKey, SelectionKey.OP_WRITE);
+ Peer peer = getPeerFromChannel((SocketChannel) socketChannel);
+ if (peer == null)
+ return null;
+
+ // Any thread that queues a message to send can set OP_WRITE,
+ // but we only allow one pending/active ChannelWriteTask per Peer
+ if (!channelsPendingWrite.add(socketChannel))
+ return null;
+
+ return new ChannelWriteTask((SocketChannel) socketChannel, peer);
+ }
+
+ if (nextSelectionKey.isAcceptable()) {
+ clearInterestOps(nextSelectionKey, SelectionKey.OP_ACCEPT);
+ return new ChannelAcceptTask((ServerSocketChannel) socketChannel);
+ }
+ } catch (CancelledKeyException e) {
+ /*
+ * Sometimes nextSelectionKey is cancelled / becomes invalid between the isValid() test at line 586
+ * and later calls to isReadable() / isWritable() / isAcceptable() which themselves call isValid()!
+ * Those isXXXable() calls could throw CancelledKeyException, so we catch it here and return null.
+ */
+ return null;
}
}
- return;
- }
- this.onPeerReady(newPeer);
+ return null;
+ }
}
- private boolean ipNotInFixedList(PeerAddress address, List fixedNetwork) {
+ public boolean ipNotInFixedList(PeerAddress address, List fixedNetwork) {
for (String ipAddress : fixedNetwork) {
String[] bits = ipAddress.split(":");
if (bits.length >= 1 && bits.length <= 2 && address.getHost().equals(bits[0])) {
@@ -750,8 +668,9 @@ public class Network {
peers.removeIf(isConnectedPeer);
// Don't consider already connected peers (resolved address match)
- // XXX This might be too slow if we end up waiting a long time for hostnames to resolve via DNS
- peers.removeIf(isResolvedAsConnectedPeer);
+ // Disabled because this might be too slow if we end up waiting a long time for hostnames to resolve via DNS
+ // Which is ok because duplicate connections to the same peer are handled during handshaking
+ // peers.removeIf(isResolvedAsConnectedPeer);
this.checkLongestConnection(now);
@@ -781,8 +700,12 @@ public class Network {
}
}
- private boolean connectPeer(Peer newPeer) throws InterruptedException {
- SocketChannel socketChannel = newPeer.connect(this.channelSelector);
+ public boolean connectPeer(Peer newPeer) throws InterruptedException {
+ // Also checked before creating PeerConnectTask
+ if (getImmutableOutboundHandshakedPeers().size() >= minOutboundPeers)
+ return false;
+
+ SocketChannel socketChannel = newPeer.connect();
if (socketChannel == null) {
return false;
}
@@ -797,7 +720,7 @@ public class Network {
return true;
}
- private Peer getPeerFromChannel(SocketChannel socketChannel) {
+ public Peer getPeerFromChannel(SocketChannel socketChannel) {
for (Peer peer : this.getImmutableConnectedPeers()) {
if (peer.getSocketChannel() == socketChannel) {
return peer;
@@ -830,7 +753,74 @@ public class Network {
nextDisconnectionCheck = now + DISCONNECTION_CHECK_INTERVAL;
}
- // Peer callbacks
+ // SocketChannel interest-ops manipulations
+
+ private static final String[] OP_NAMES = new String[SelectionKey.OP_ACCEPT * 2];
+ static {
+ for (int i = 0; i < OP_NAMES.length; i++) {
+ StringJoiner joiner = new StringJoiner(",");
+
+ if ((i & SelectionKey.OP_READ) != 0) joiner.add("OP_READ");
+ if ((i & SelectionKey.OP_WRITE) != 0) joiner.add("OP_WRITE");
+ if ((i & SelectionKey.OP_CONNECT) != 0) joiner.add("OP_CONNECT");
+ if ((i & SelectionKey.OP_ACCEPT) != 0) joiner.add("OP_ACCEPT");
+
+ OP_NAMES[i] = joiner.toString();
+ }
+ }
+
+ public void clearInterestOps(SelectableChannel socketChannel, int interestOps) {
+ SelectionKey selectionKey = socketChannel.keyFor(channelSelector);
+ if (selectionKey == null)
+ return;
+
+ clearInterestOps(selectionKey, interestOps);
+ }
+
+ private void clearInterestOps(SelectionKey selectionKey, int interestOps) {
+ if (!selectionKey.channel().isOpen())
+ return;
+
+ LOGGER.trace("Thread {} clearing {} interest-ops on channel: {}",
+ Thread.currentThread().getId(),
+ OP_NAMES[interestOps],
+ selectionKey.channel());
+
+ selectionKey.interestOpsAnd(~interestOps);
+ }
+
+ public void setInterestOps(SelectableChannel socketChannel, int interestOps) {
+ SelectionKey selectionKey = socketChannel.keyFor(channelSelector);
+ if (selectionKey == null) {
+ try {
+ selectionKey = socketChannel.register(this.channelSelector, interestOps);
+ } catch (ClosedChannelException e) {
+ // Channel already closed so ignore
+ return;
+ }
+ // Fall-through to allow logging
+ }
+
+ setInterestOps(selectionKey, interestOps);
+ }
+
+ private void setInterestOps(SelectionKey selectionKey, int interestOps) {
+ if (!selectionKey.channel().isOpen())
+ return;
+
+ LOGGER.trace("Thread {} setting {} interest-ops on channel: {}",
+ Thread.currentThread().getId(),
+ OP_NAMES[interestOps],
+ selectionKey.channel());
+
+ selectionKey.interestOpsOr(interestOps);
+ }
+
+ // Peer / Task callbacks
+
+ public void notifyChannelNotWriting(SelectableChannel socketChannel) {
+ this.channelsPendingWrite.remove(socketChannel);
+ }
protected void wakeupChannelSelector() {
this.channelSelector.wakeup();
@@ -856,8 +846,6 @@ public class Network {
}
public void onDisconnect(Peer peer) {
- // Notify Controller
- Controller.getInstance().onPeerDisconnect(peer);
if (peer.getConnectionEstablishedTime() > 0L) {
LOGGER.debug("[{}] Disconnected from peer {}", peer.getPeerConnectionId(), peer);
} else {
@@ -865,6 +853,25 @@ public class Network {
}
this.removeConnectedPeer(peer);
+ this.channelsPendingWrite.remove(peer.getSocketChannel());
+
+ if (this.isShuttingDown)
+ // No need to do any further processing, like re-enabling listen socket or notifying Controller
+ return;
+
+ if (getImmutableConnectedPeers().size() < maxPeers - 1
+ && serverSelectionKey.isValid()
+ && (serverSelectionKey.interestOps() & SelectionKey.OP_ACCEPT) == 0) {
+ try {
+ LOGGER.debug("Re-enabling accepting incoming connections because the server is not longer full");
+ setInterestOps(serverSelectionKey, SelectionKey.OP_ACCEPT);
+ } catch (CancelledKeyException e) {
+ LOGGER.error("Failed to re-enable accepting of incoming connections: {}", e.getMessage());
+ }
+ }
+
+ // Notify Controller
+ Controller.getInstance().onPeerDisconnect(peer);
}
public void peerMisbehaved(Peer peer) {
@@ -1302,8 +1309,9 @@ public class Network {
try {
InetSocketAddress knownAddress = peerAddress.toSocketAddress();
- List peers = this.getImmutableConnectedPeers();
- peers.removeIf(peer -> !Peer.addressEquals(knownAddress, peer.getResolvedAddress()));
+ List peers = this.getImmutableConnectedPeers().stream()
+ .filter(peer -> Peer.addressEquals(knownAddress, peer.getResolvedAddress()))
+ .collect(Collectors.toList());
for (Peer peer : peers) {
peer.disconnect("to be forgotten");
@@ -1461,54 +1469,27 @@ public class Network {
}
public void broadcast(Function peerMessageBuilder) {
- class Broadcaster implements Runnable {
- private final Random random = new Random();
+ for (Peer peer : getImmutableHandshakedPeers()) {
+ if (this.isShuttingDown)
+ return;
- private List targetPeers;
- private Function peerMessageBuilder;
+ Message message = peerMessageBuilder.apply(peer);
- Broadcaster(List targetPeers, Function peerMessageBuilder) {
- this.targetPeers = targetPeers;
- this.peerMessageBuilder = peerMessageBuilder;
+ if (message == null) {
+ continue;
}
- @Override
- public void run() {
- Thread.currentThread().setName("Network Broadcast");
-
- for (Peer peer : targetPeers) {
- // Very short sleep to reduce strain, improve multi-threading and catch interrupts
- try {
- Thread.sleep(random.nextInt(20) + 20L);
- } catch (InterruptedException e) {
- break;
- }
-
- Message message = peerMessageBuilder.apply(peer);
-
- if (message == null) {
- continue;
- }
-
- if (!peer.sendMessage(message)) {
- peer.disconnect("failed to broadcast message");
- }
- }
-
- Thread.currentThread().setName("Network Broadcast (dormant)");
+ if (!peer.sendMessage(message)) {
+ peer.disconnect("failed to broadcast message");
}
}
-
- try {
- broadcastExecutor.execute(new Broadcaster(this.getImmutableHandshakedPeers(), peerMessageBuilder));
- } catch (RejectedExecutionException e) {
- // Can't execute - probably because we're shutting down, so ignore
- }
}
// Shutdown
public void shutdown() {
+ this.isShuttingDown = true;
+
// Close listen socket to prevent more incoming connections
if (this.serverChannel.isOpen()) {
try {
@@ -1527,16 +1508,6 @@ public class Network {
LOGGER.warn("Interrupted while waiting for networking threads to terminate");
}
- // Stop broadcasts
- this.broadcastExecutor.shutdownNow();
- try {
- if (!this.broadcastExecutor.awaitTermination(1000, TimeUnit.MILLISECONDS)) {
- LOGGER.warn("Broadcast threads failed to terminate");
- }
- } catch (InterruptedException e) {
- LOGGER.warn("Interrupted while waiting for broadcast threads failed to terminate");
- }
-
// Close all peer connections
for (Peer peer : this.getImmutableConnectedPeers()) {
peer.shutdown();
diff --git a/src/main/java/org/qortal/network/Peer.java b/src/main/java/org/qortal/network/Peer.java
index da4a70a9..dbb03fda 100644
--- a/src/main/java/org/qortal/network/Peer.java
+++ b/src/main/java/org/qortal/network/Peer.java
@@ -11,25 +11,21 @@ import org.qortal.data.network.PeerChainTipData;
import org.qortal.data.network.PeerData;
import org.qortal.network.message.ChallengeMessage;
import org.qortal.network.message.Message;
-import org.qortal.network.message.Message.MessageException;
-import org.qortal.network.message.Message.MessageType;
-import org.qortal.network.message.PingMessage;
+import org.qortal.network.message.MessageException;
+import org.qortal.network.task.MessageTask;
+import org.qortal.network.task.PingTask;
import org.qortal.settings.Settings;
-import org.qortal.utils.ExecuteProduceConsume;
+import org.qortal.utils.ExecuteProduceConsume.Task;
import org.qortal.utils.NTP;
import java.io.IOException;
import java.net.*;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
-import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.security.SecureRandom;
import java.util.*;
-import java.util.concurrent.ArrayBlockingQueue;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.TimeUnit;
+import java.util.concurrent.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -48,9 +44,9 @@ public class Peer {
private static final int RESPONSE_TIMEOUT = 3000; // ms
/**
- * Maximum time to wait for a peer to respond with blocks (ms)
+ * Maximum time to wait for a message to be added to sendQueue (ms)
*/
- public static final int FETCH_BLOCKS_TIMEOUT = 10000;
+ private static final int QUEUE_TIMEOUT = 1000; // ms
/**
* Interval between PING messages to a peer. (ms)
@@ -71,10 +67,14 @@ public class Peer {
private final UUID peerConnectionId = UUID.randomUUID();
private final Object byteBufferLock = new Object();
private ByteBuffer byteBuffer;
-
private Map> replyQueues;
private LinkedBlockingQueue pendingMessages;
+ private TransferQueue sendQueue;
+ private ByteBuffer outputBuffer;
+ private String outputMessageType;
+ private int outputMessageId;
+
/**
* True if we created connection to peer, false if we accepted incoming connection from peer.
*/
@@ -98,7 +98,7 @@ public class Peer {
/**
* When last PING message was sent, or null if pings not started yet.
*/
- private Long lastPingSent;
+ private Long lastPingSent = null;
byte[] ourChallenge;
@@ -160,10 +160,10 @@ public class Peer {
/**
* Construct Peer using existing, connected socket
*/
- public Peer(SocketChannel socketChannel, Selector channelSelector) throws IOException {
+ public Peer(SocketChannel socketChannel) throws IOException {
this.isOutbound = false;
this.socketChannel = socketChannel;
- sharedSetup(channelSelector);
+ sharedSetup();
this.resolvedAddress = ((InetSocketAddress) socketChannel.socket().getRemoteSocketAddress());
this.isLocal = isAddressLocal(this.resolvedAddress.getAddress());
@@ -276,7 +276,7 @@ public class Peer {
}
}
- protected void setLastPing(long lastPing) {
+ public void setLastPing(long lastPing) {
synchronized (this.peerInfoLock) {
this.lastPing = lastPing;
}
@@ -346,12 +346,6 @@ public class Peer {
}
}
- protected void queueMessage(Message message) {
- if (!this.pendingMessages.offer(message)) {
- LOGGER.info("[{}] No room to queue message from peer {} - discarding", this.peerConnectionId, this);
- }
- }
-
public boolean isSyncInProgress() {
return this.syncInProgress;
}
@@ -396,13 +390,14 @@ public class Peer {
// Processing
- private void sharedSetup(Selector channelSelector) throws IOException {
+ private void sharedSetup() throws IOException {
this.connectionTimestamp = NTP.getTime();
this.socketChannel.setOption(StandardSocketOptions.TCP_NODELAY, true);
this.socketChannel.configureBlocking(false);
- this.socketChannel.register(channelSelector, SelectionKey.OP_READ);
+ Network.getInstance().setInterestOps(this.socketChannel, SelectionKey.OP_READ);
this.byteBuffer = null; // Defer allocation to when we need it, to save memory. Sorry GC!
- this.replyQueues = Collections.synchronizedMap(new HashMap>());
+ this.sendQueue = new LinkedTransferQueue<>();
+ this.replyQueues = new ConcurrentHashMap<>();
this.pendingMessages = new LinkedBlockingQueue<>();
Random random = new SecureRandom();
@@ -410,7 +405,7 @@ public class Peer {
random.nextBytes(this.ourChallenge);
}
- public SocketChannel connect(Selector channelSelector) {
+ public SocketChannel connect() {
LOGGER.trace("[{}] Connecting to peer {}", this.peerConnectionId, this);
try {
@@ -418,6 +413,8 @@ public class Peer {
this.isLocal = isAddressLocal(this.resolvedAddress.getAddress());
this.socketChannel = SocketChannel.open();
+ InetAddress bindAddr = InetAddress.getByName(Settings.getInstance().getBindAddress());
+ this.socketChannel.socket().bind(new InetSocketAddress(bindAddr, 0));
this.socketChannel.socket().connect(resolvedAddress, CONNECT_TIMEOUT);
} catch (SocketTimeoutException e) {
LOGGER.trace("[{}] Connection timed out to peer {}", this.peerConnectionId, this);
@@ -432,7 +429,7 @@ public class Peer {
try {
LOGGER.debug("[{}] Connected to peer {}", this.peerConnectionId, this);
- sharedSetup(channelSelector);
+ sharedSetup();
return socketChannel;
} catch (IOException e) {
LOGGER.trace("[{}] Post-connection setup failed, peer {}", this.peerConnectionId, this);
@@ -450,7 +447,7 @@ public class Peer {
*
* @throws IOException If this channel is not yet connected
*/
- protected void readChannel() throws IOException {
+ public void readChannel() throws IOException {
synchronized (this.byteBufferLock) {
while (true) {
if (!this.socketChannel.isOpen() || this.socketChannel.socket().isClosed()) {
@@ -556,7 +553,67 @@ public class Peer {
}
}
- protected ExecuteProduceConsume.Task getMessageTask() {
+ /** Maybe send some pending outgoing messages.
+ *
+ * @return true if more data is pending to be sent
+ */
+ public boolean writeChannel() throws IOException {
+ // It is the responsibility of ChannelWriteTask's producer to produce only one call to writeChannel() at a time
+
+ while (true) {
+ // If output byte buffer is null, fetch next message from queue (if any)
+ while (this.outputBuffer == null) {
+ Message message;
+
+ try {
+ // Allow other thread time to add message to queue having raised OP_WRITE.
+ // Timeout is overkill but not excessive enough to clog up networking / EPC.
+ // This is to avoid race condition in sendMessageWithTimeout() below.
+ message = this.sendQueue.poll(QUEUE_TIMEOUT, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ // Shutdown situation
+ return false;
+ }
+
+ // No message? No further work to be done
+ if (message == null)
+ return false;
+
+ try {
+ this.outputBuffer = ByteBuffer.wrap(message.toBytes());
+ this.outputMessageType = message.getType().name();
+ this.outputMessageId = message.getId();
+
+ LOGGER.trace("[{}] Sending {} message with ID {} to peer {}",
+ this.peerConnectionId, this.outputMessageType, this.outputMessageId, this);
+ } catch (MessageException e) {
+ // Something went wrong converting message to bytes, so discard but allow another round
+ LOGGER.warn("[{}] Failed to send {} message with ID {} to peer {}: {}", this.peerConnectionId,
+ message.getType().name(), message.getId(), this, e.getMessage());
+ }
+ }
+
+ // If output byte buffer is not null, send from that
+ int bytesWritten = this.socketChannel.write(outputBuffer);
+
+ LOGGER.trace("[{}] Sent {} bytes of {} message with ID {} to peer {} ({} total)", this.peerConnectionId,
+ bytesWritten, this.outputMessageType, this.outputMessageId, this, outputBuffer.limit());
+
+ // If we've sent 0 bytes then socket buffer is full so we need to wait until it's empty again
+ if (bytesWritten == 0) {
+ return true;
+ }
+
+ // If we then exhaust the byte buffer, set it to null (otherwise loop and try to send more)
+ if (!this.outputBuffer.hasRemaining()) {
+ this.outputMessageType = null;
+ this.outputMessageId = 0;
+ this.outputBuffer = null;
+ }
+ }
+ }
+
+ protected Task getMessageTask() {
/*
* If we are still handshaking and there is a message yet to be processed then
* don't produce another message task. This allows us to process handshake
@@ -580,7 +637,7 @@ public class Peer {
}
// Return a task to process message in queue
- return () -> Network.getInstance().onMessage(this, nextMessage);
+ return new MessageTask(this, nextMessage);
}
/**
@@ -605,54 +662,25 @@ public class Peer {
}
try {
- // Send message
- LOGGER.trace("[{}] Sending {} message with ID {} to peer {}", this.peerConnectionId,
+ // Queue message, to be picked up by ChannelWriteTask and then peer.writeChannel()
+ LOGGER.trace("[{}] Queuing {} message with ID {} to peer {}", this.peerConnectionId,
message.getType().name(), message.getId(), this);
- ByteBuffer outputBuffer = ByteBuffer.wrap(message.toBytes());
+ // Check message properly constructed
+ message.checkValidOutgoing();
- synchronized (this.socketChannel) {
- final long sendStart = System.currentTimeMillis();
- long totalBytes = 0;
-
- while (outputBuffer.hasRemaining()) {
- int bytesWritten = this.socketChannel.write(outputBuffer);
- totalBytes += bytesWritten;
-
- LOGGER.trace("[{}] Sent {} bytes of {} message with ID {} to peer {} ({} total)", this.peerConnectionId,
- bytesWritten, message.getType().name(), message.getId(), this, totalBytes);
-
- if (bytesWritten == 0) {
- // Underlying socket's internal buffer probably full,
- // so wait a short while for bytes to actually be transmitted over the wire
-
- /*
- * NOSONAR squid:S2276 - we don't want to use this.socketChannel.wait()
- * as this releases the lock held by synchronized() above
- * and would allow another thread to send another message,
- * potentially interleaving them on-the-wire, causing checksum failures
- * and connection loss.
- */
- Thread.sleep(1L); //NOSONAR squid:S2276
-
- if (System.currentTimeMillis() - sendStart > timeout) {
- // We've taken too long to send this message
- return false;
- }
- }
- }
- }
- } catch (MessageException e) {
- LOGGER.warn("[{}] Failed to send {} message with ID {} to peer {}: {}", this.peerConnectionId,
- message.getType().name(), message.getId(), this, e.getMessage());
- return false;
- } catch (IOException | InterruptedException e) {
+ // Possible race condition:
+ // We set OP_WRITE, EPC creates ChannelWriteTask which calls Peer.writeChannel, writeChannel's poll() finds no message to send
+ // Avoided by poll-with-timeout in writeChannel() above.
+ Network.getInstance().setInterestOps(this.socketChannel, SelectionKey.OP_WRITE);
+ return this.sendQueue.tryTransfer(message, timeout, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
// Send failure
return false;
+ } catch (MessageException e) {
+ LOGGER.error(e.getMessage(), e);
+ return false;
}
-
- // Sent OK
- return true;
}
/**
@@ -720,7 +748,7 @@ public class Peer {
this.lastPingSent = NTP.getTime();
}
- protected ExecuteProduceConsume.Task getPingTask(Long now) {
+ protected Task getPingTask(Long now) {
// Pings not enabled yet?
if (now == null || this.lastPingSent == null) {
return null;
@@ -734,19 +762,7 @@ public class Peer {
// Not strictly true, but prevents this peer from being immediately chosen again
this.lastPingSent = now;
- return () -> {
- PingMessage pingMessage = new PingMessage();
- Message message = this.getResponse(pingMessage);
-
- if (message == null || message.getType() != MessageType.PING) {
- LOGGER.debug("[{}] Didn't receive reply from {} for PING ID {}", this.peerConnectionId, this,
- pingMessage.getId());
- this.disconnect("no ping received");
- return;
- }
-
- this.setLastPing(NTP.getTime() - now);
- };
+ return new PingTask(this, now);
}
public void disconnect(String reason) {
diff --git a/src/main/java/org/qortal/network/message/ArbitraryDataFileListMessage.java b/src/main/java/org/qortal/network/message/ArbitraryDataFileListMessage.java
index 32ba3fa7..ed3cae76 100644
--- a/src/main/java/org/qortal/network/message/ArbitraryDataFileListMessage.java
+++ b/src/main/java/org/qortal/network/message/ArbitraryDataFileListMessage.java
@@ -9,38 +9,59 @@ import org.qortal.utils.Serialization;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
public class ArbitraryDataFileListMessage extends Message {
- private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
- private static final int HASH_LENGTH = Transformer.SHA256_LENGTH;
- private static final int MAX_PEER_ADDRESS_LENGTH = PeerData.MAX_PEER_ADDRESS_SIZE;
-
- private final byte[] signature;
- private final List hashes;
+ private byte[] signature;
+ private List hashes;
private Long requestTime;
private Integer requestHops;
private String peerAddress;
private Boolean isRelayPossible;
-
public ArbitraryDataFileListMessage(byte[] signature, List hashes, Long requestTime,
- Integer requestHops, String peerAddress, boolean isRelayPossible) {
+ Integer requestHops, String peerAddress, Boolean isRelayPossible) {
super(MessageType.ARBITRARY_DATA_FILE_LIST);
- this.signature = signature;
- this.hashes = hashes;
- this.requestTime = requestTime;
- this.requestHops = requestHops;
- this.peerAddress = peerAddress;
- this.isRelayPossible = isRelayPossible;
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+
+ try {
+ bytes.write(signature);
+
+ bytes.write(Ints.toByteArray(hashes.size()));
+
+ for (byte[] hash : hashes) {
+ bytes.write(hash);
+ }
+
+ if (requestTime != null) {
+ // The remaining fields are optional
+
+ bytes.write(Longs.toByteArray(requestTime));
+
+ bytes.write(Ints.toByteArray(requestHops));
+
+ Serialization.serializeSizedStringV2(bytes, peerAddress);
+
+ bytes.write(Ints.toByteArray(Boolean.TRUE.equals(isRelayPossible) ? 1 : 0));
+ }
+ } catch (IOException e) {
+ throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
+ }
+
+ this.dataBytes = bytes.toByteArray();
+ this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
- public ArbitraryDataFileListMessage(int id, byte[] signature, List hashes, Long requestTime,
+ /** Legacy version */
+ public ArbitraryDataFileListMessage(byte[] signature, List hashes) {
+ this(signature, hashes, null, null, null, null);
+ }
+
+ private ArbitraryDataFileListMessage(int id, byte[] signature, List hashes, Long requestTime,
Integer requestHops, String peerAddress, boolean isRelayPossible) {
super(id, MessageType.ARBITRARY_DATA_FILE_LIST);
@@ -52,24 +73,39 @@ public class ArbitraryDataFileListMessage extends Message {
this.isRelayPossible = isRelayPossible;
}
- public List getHashes() {
- return this.hashes;
- }
-
public byte[] getSignature() {
return this.signature;
}
- public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException, TransformationException {
- byte[] signature = new byte[SIGNATURE_LENGTH];
+ public List getHashes() {
+ return this.hashes;
+ }
+
+ public Long getRequestTime() {
+ return this.requestTime;
+ }
+
+ public Integer getRequestHops() {
+ return this.requestHops;
+ }
+
+ public String getPeerAddress() {
+ return this.peerAddress;
+ }
+
+ public Boolean isRelayPossible() {
+ return this.isRelayPossible;
+ }
+
+ public static Message fromByteBuffer(int id, ByteBuffer bytes) throws MessageException {
+ byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
bytes.get(signature);
int count = bytes.getInt();
List hashes = new ArrayList<>();
for (int i = 0; i < count; ++i) {
-
- byte[] hash = new byte[HASH_LENGTH];
+ byte[] hash = new byte[Transformer.SHA256_LENGTH];
bytes.get(hash);
hashes.add(hash);
}
@@ -80,99 +116,21 @@ public class ArbitraryDataFileListMessage extends Message {
boolean isRelayPossible = true; // Legacy versions only send this message when relaying is possible
// The remaining fields are optional
-
if (bytes.hasRemaining()) {
+ try {
+ requestTime = bytes.getLong();
- requestTime = bytes.getLong();
+ requestHops = bytes.getInt();
- requestHops = bytes.getInt();
-
- peerAddress = Serialization.deserializeSizedStringV2(bytes, MAX_PEER_ADDRESS_LENGTH);
-
- isRelayPossible = bytes.getInt() > 0;
+ peerAddress = Serialization.deserializeSizedStringV2(bytes, PeerData.MAX_PEER_ADDRESS_SIZE);
+ isRelayPossible = bytes.getInt() > 0;
+ } catch (TransformationException e) {
+ throw new MessageException(e.getMessage(), e);
+ }
}
return new ArbitraryDataFileListMessage(id, signature, hashes, requestTime, requestHops, peerAddress, isRelayPossible);
}
- @Override
- protected byte[] toData() {
- try {
- ByteArrayOutputStream bytes = new ByteArrayOutputStream();
-
- bytes.write(this.signature);
-
- bytes.write(Ints.toByteArray(this.hashes.size()));
-
- for (byte[] hash : this.hashes) {
- bytes.write(hash);
- }
-
- if (this.requestTime == null) { // To maintain backwards support
- return bytes.toByteArray();
- }
-
- // The remaining fields are optional
-
- bytes.write(Longs.toByteArray(this.requestTime));
-
- bytes.write(Ints.toByteArray(this.requestHops));
-
- Serialization.serializeSizedStringV2(bytes, this.peerAddress);
-
- bytes.write(Ints.toByteArray(this.isRelayPossible ? 1 : 0));
-
- return bytes.toByteArray();
- } catch (IOException e) {
- return null;
- }
- }
-
- public ArbitraryDataFileListMessage cloneWithNewId(int newId) {
- ArbitraryDataFileListMessage clone = new ArbitraryDataFileListMessage(this.signature, this.hashes,
- this.requestTime, this.requestHops, this.peerAddress, this.isRelayPossible);
- clone.setId(newId);
- return clone;
- }
-
- public void removeOptionalStats() {
- this.requestTime = null;
- this.requestHops = null;
- this.peerAddress = null;
- this.isRelayPossible = null;
- }
-
- public Long getRequestTime() {
- return this.requestTime;
- }
-
- public void setRequestTime(Long requestTime) {
- this.requestTime = requestTime;
- }
-
- public Integer getRequestHops() {
- return this.requestHops;
- }
-
- public void setRequestHops(Integer requestHops) {
- this.requestHops = requestHops;
- }
-
- public String getPeerAddress() {
- return this.peerAddress;
- }
-
- public void setPeerAddress(String peerAddress) {
- this.peerAddress = peerAddress;
- }
-
- public Boolean isRelayPossible() {
- return this.isRelayPossible;
- }
-
- public void setIsRelayPossible(Boolean isRelayPossible) {
- this.isRelayPossible = isRelayPossible;
- }
-
}
diff --git a/src/main/java/org/qortal/network/message/ArbitraryDataFileMessage.java b/src/main/java/org/qortal/network/message/ArbitraryDataFileMessage.java
index b9f24e29..50991be3 100644
--- a/src/main/java/org/qortal/network/message/ArbitraryDataFileMessage.java
+++ b/src/main/java/org/qortal/network/message/ArbitraryDataFileMessage.java
@@ -9,44 +9,60 @@ import org.qortal.transform.Transformer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.io.UnsupportedEncodingException;
+import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
public class ArbitraryDataFileMessage extends Message {
private static final Logger LOGGER = LogManager.getLogger(ArbitraryDataFileMessage.class);
- private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
-
- private final byte[] signature;
- private final ArbitraryDataFile arbitraryDataFile;
+ private byte[] signature;
+ private ArbitraryDataFile arbitraryDataFile;
public ArbitraryDataFileMessage(byte[] signature, ArbitraryDataFile arbitraryDataFile) {
super(MessageType.ARBITRARY_DATA_FILE);
- this.signature = signature;
- this.arbitraryDataFile = arbitraryDataFile;
+ byte[] data = arbitraryDataFile.getBytes();
+
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+
+ try {
+ bytes.write(signature);
+
+ bytes.write(Ints.toByteArray(data.length));
+
+ bytes.write(data);
+ } catch (IOException e) {
+ throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
+ }
+
+ this.dataBytes = bytes.toByteArray();
+ this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
- public ArbitraryDataFileMessage(int id, byte[] signature, ArbitraryDataFile arbitraryDataFile) {
+ private ArbitraryDataFileMessage(int id, byte[] signature, ArbitraryDataFile arbitraryDataFile) {
super(id, MessageType.ARBITRARY_DATA_FILE);
this.signature = signature;
this.arbitraryDataFile = arbitraryDataFile;
}
+ public byte[] getSignature() {
+ return this.signature;
+ }
+
public ArbitraryDataFile getArbitraryDataFile() {
return this.arbitraryDataFile;
}
- public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws UnsupportedEncodingException {
- byte[] signature = new byte[SIGNATURE_LENGTH];
+ public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws MessageException {
+ byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
byteBuffer.get(signature);
int dataLength = byteBuffer.getInt();
- if (byteBuffer.remaining() != dataLength)
- return null;
+ if (byteBuffer.remaining() < dataLength)
+ throw new BufferUnderflowException();
byte[] data = new byte[dataLength];
byteBuffer.get(data);
@@ -54,43 +70,10 @@ public class ArbitraryDataFileMessage extends Message {
try {
ArbitraryDataFile arbitraryDataFile = new ArbitraryDataFile(data, signature);
return new ArbitraryDataFileMessage(id, signature, arbitraryDataFile);
- }
- catch (DataException e) {
+ } catch (DataException e) {
LOGGER.info("Unable to process received file: {}", e.getMessage());
- return null;
+ throw new MessageException("Unable to process received file: " + e.getMessage(), e);
}
}
- @Override
- protected byte[] toData() {
- if (this.arbitraryDataFile == null) {
- return null;
- }
-
- byte[] data = this.arbitraryDataFile.getBytes();
- if (data == null) {
- return null;
- }
-
- try {
- ByteArrayOutputStream bytes = new ByteArrayOutputStream();
-
- bytes.write(signature);
-
- bytes.write(Ints.toByteArray(data.length));
-
- bytes.write(data);
-
- return bytes.toByteArray();
- } catch (IOException e) {
- return null;
- }
- }
-
- public ArbitraryDataFileMessage cloneWithNewId(int newId) {
- ArbitraryDataFileMessage clone = new ArbitraryDataFileMessage(this.signature, this.arbitraryDataFile);
- clone.setId(newId);
- return clone;
- }
-
}
diff --git a/src/main/java/org/qortal/network/message/ArbitraryDataMessage.java b/src/main/java/org/qortal/network/message/ArbitraryDataMessage.java
index 1ce149f7..142e35cc 100644
--- a/src/main/java/org/qortal/network/message/ArbitraryDataMessage.java
+++ b/src/main/java/org/qortal/network/message/ArbitraryDataMessage.java
@@ -2,7 +2,7 @@ package org.qortal.network.message;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.io.UnsupportedEncodingException;
+import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import org.qortal.transform.Transformer;
@@ -11,13 +11,26 @@ import com.google.common.primitives.Ints;
public class ArbitraryDataMessage extends Message {
- private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
-
private byte[] signature;
private byte[] data;
public ArbitraryDataMessage(byte[] signature, byte[] data) {
- this(-1, signature, data);
+ super(MessageType.ARBITRARY_DATA);
+
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+
+ try {
+ bytes.write(signature);
+
+ bytes.write(Ints.toByteArray(data.length));
+
+ bytes.write(data);
+ } catch (IOException e) {
+ throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
+ }
+
+ this.dataBytes = bytes.toByteArray();
+ this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private ArbitraryDataMessage(int id, byte[] signature, byte[] data) {
@@ -35,14 +48,14 @@ public class ArbitraryDataMessage extends Message {
return this.data;
}
- public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws UnsupportedEncodingException {
- byte[] signature = new byte[SIGNATURE_LENGTH];
+ public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) {
+ byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
byteBuffer.get(signature);
int dataLength = byteBuffer.getInt();
- if (byteBuffer.remaining() != dataLength)
- return null;
+ if (byteBuffer.remaining() < dataLength)
+ throw new BufferUnderflowException();
byte[] data = new byte[dataLength];
byteBuffer.get(data);
@@ -50,24 +63,4 @@ public class ArbitraryDataMessage extends Message {
return new ArbitraryDataMessage(id, signature, data);
}
- @Override
- protected byte[] toData() {
- if (this.data == null)
- return null;
-
- try {
- ByteArrayOutputStream bytes = new ByteArrayOutputStream();
-
- bytes.write(this.signature);
-
- bytes.write(Ints.toByteArray(this.data.length));
-
- bytes.write(this.data);
-
- return bytes.toByteArray();
- } catch (IOException e) {
- return null;
- }
- }
-
}
diff --git a/src/main/java/org/qortal/network/message/ArbitraryMetadataMessage.java b/src/main/java/org/qortal/network/message/ArbitraryMetadataMessage.java
index 9228d458..26601d4b 100644
--- a/src/main/java/org/qortal/network/message/ArbitraryMetadataMessage.java
+++ b/src/main/java/org/qortal/network/message/ArbitraryMetadataMessage.java
@@ -7,28 +7,40 @@ import org.qortal.transform.Transformer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.io.UnsupportedEncodingException;
+import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
public class ArbitraryMetadataMessage extends Message {
- private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
+ private byte[] signature;
+ private ArbitraryDataFile arbitraryMetadataFile;
- private final byte[] signature;
- private final ArbitraryDataFile arbitraryMetadataFile;
-
- public ArbitraryMetadataMessage(byte[] signature, ArbitraryDataFile arbitraryDataFile) {
+ public ArbitraryMetadataMessage(byte[] signature, ArbitraryDataFile arbitraryMetadataFile) {
super(MessageType.ARBITRARY_METADATA);
- this.signature = signature;
- this.arbitraryMetadataFile = arbitraryDataFile;
+ byte[] data = arbitraryMetadataFile.getBytes();
+
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+
+ try {
+ bytes.write(signature);
+
+ bytes.write(Ints.toByteArray(data.length));
+
+ bytes.write(data);
+ } catch (IOException e) {
+ throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
+ }
+
+ this.dataBytes = bytes.toByteArray();
+ this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
- public ArbitraryMetadataMessage(int id, byte[] signature, ArbitraryDataFile arbitraryDataFile) {
+ private ArbitraryMetadataMessage(int id, byte[] signature, ArbitraryDataFile arbitraryMetadataFile) {
super(id, MessageType.ARBITRARY_METADATA);
this.signature = signature;
- this.arbitraryMetadataFile = arbitraryDataFile;
+ this.arbitraryMetadataFile = arbitraryMetadataFile;
}
public byte[] getSignature() {
@@ -39,14 +51,14 @@ public class ArbitraryMetadataMessage extends Message {
return this.arbitraryMetadataFile;
}
- public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws UnsupportedEncodingException {
- byte[] signature = new byte[SIGNATURE_LENGTH];
+ public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws MessageException {
+ byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
byteBuffer.get(signature);
int dataLength = byteBuffer.getInt();
- if (byteBuffer.remaining() != dataLength)
- return null;
+ if (byteBuffer.remaining() < dataLength)
+ throw new BufferUnderflowException();
byte[] data = new byte[dataLength];
byteBuffer.get(data);
@@ -54,42 +66,9 @@ public class ArbitraryMetadataMessage extends Message {
try {
ArbitraryDataFile arbitraryMetadataFile = new ArbitraryDataFile(data, signature);
return new ArbitraryMetadataMessage(id, signature, arbitraryMetadataFile);
+ } catch (DataException e) {
+ throw new MessageException("Unable to process arbitrary metadata message: " + e.getMessage(), e);
}
- catch (DataException e) {
- return null;
- }
- }
-
- @Override
- protected byte[] toData() {
- if (this.arbitraryMetadataFile == null) {
- return null;
- }
-
- byte[] data = this.arbitraryMetadataFile.getBytes();
- if (data == null) {
- return null;
- }
-
- try {
- ByteArrayOutputStream bytes = new ByteArrayOutputStream();
-
- bytes.write(signature);
-
- bytes.write(Ints.toByteArray(data.length));
-
- bytes.write(data);
-
- return bytes.toByteArray();
- } catch (IOException e) {
- return null;
- }
- }
-
- public ArbitraryMetadataMessage cloneWithNewId(int newId) {
- ArbitraryMetadataMessage clone = new ArbitraryMetadataMessage(this.signature, this.arbitraryMetadataFile);
- clone.setId(newId);
- return clone;
}
}
diff --git a/src/main/java/org/qortal/network/message/ArbitrarySignaturesMessage.java b/src/main/java/org/qortal/network/message/ArbitrarySignaturesMessage.java
index 1f980b3c..aa75b2a1 100644
--- a/src/main/java/org/qortal/network/message/ArbitrarySignaturesMessage.java
+++ b/src/main/java/org/qortal/network/message/ArbitrarySignaturesMessage.java
@@ -8,21 +8,37 @@ import org.qortal.utils.Serialization;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.io.UnsupportedEncodingException;
+import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
public class ArbitrarySignaturesMessage extends Message {
- private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
-
private String peerAddress;
private int requestHops;
private List signatures;
public ArbitrarySignaturesMessage(String peerAddress, int requestHops, List signatures) {
- this(-1, peerAddress, requestHops, signatures);
+ super(MessageType.ARBITRARY_SIGNATURES);
+
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+
+ try {
+ Serialization.serializeSizedStringV2(bytes, peerAddress);
+
+ bytes.write(Ints.toByteArray(requestHops));
+
+ bytes.write(Ints.toByteArray(signatures.size()));
+
+ for (byte[] signature : signatures)
+ bytes.write(signature);
+ } catch (IOException e) {
+ throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
+ }
+
+ this.dataBytes = bytes.toByteArray();
+ this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private ArbitrarySignaturesMessage(int id, String peerAddress, int requestHops, List signatures) {
@@ -41,27 +57,24 @@ public class ArbitrarySignaturesMessage extends Message {
return this.signatures;
}
- public int getRequestHops() {
- return this.requestHops;
- }
-
- public void setRequestHops(int requestHops) {
- this.requestHops = requestHops;
- }
-
- public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException, TransformationException {
- String peerAddress = Serialization.deserializeSizedStringV2(bytes, PeerData.MAX_PEER_ADDRESS_SIZE);
+ public static Message fromByteBuffer(int id, ByteBuffer bytes) throws MessageException {
+ String peerAddress;
+ try {
+ peerAddress = Serialization.deserializeSizedStringV2(bytes, PeerData.MAX_PEER_ADDRESS_SIZE);
+ } catch (TransformationException e) {
+ throw new MessageException(e.getMessage(), e);
+ }
int requestHops = bytes.getInt();
int signatureCount = bytes.getInt();
- if (bytes.remaining() != signatureCount * SIGNATURE_LENGTH)
- return null;
+ if (bytes.remaining() < signatureCount * Transformer.SIGNATURE_LENGTH)
+ throw new BufferUnderflowException();
List signatures = new ArrayList<>();
for (int i = 0; i < signatureCount; ++i) {
- byte[] signature = new byte[SIGNATURE_LENGTH];
+ byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
bytes.get(signature);
signatures.add(signature);
}
@@ -69,24 +82,4 @@ public class ArbitrarySignaturesMessage extends Message {
return new ArbitrarySignaturesMessage(id, peerAddress, requestHops, signatures);
}
- @Override
- protected byte[] toData() {
- try {
- ByteArrayOutputStream bytes = new ByteArrayOutputStream();
-
- Serialization.serializeSizedStringV2(bytes, this.peerAddress);
-
- bytes.write(Ints.toByteArray(this.requestHops));
-
- bytes.write(Ints.toByteArray(this.signatures.size()));
-
- for (byte[] signature : this.signatures)
- bytes.write(signature);
-
- return bytes.toByteArray();
- } catch (IOException e) {
- return null;
- }
- }
-
}
diff --git a/src/main/java/org/qortal/network/message/BlockMessage.java b/src/main/java/org/qortal/network/message/BlockMessage.java
index b07dc8b1..2dd4db87 100644
--- a/src/main/java/org/qortal/network/message/BlockMessage.java
+++ b/src/main/java/org/qortal/network/message/BlockMessage.java
@@ -1,14 +1,10 @@
package org.qortal.network.message;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.List;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
-import org.qortal.block.Block;
import org.qortal.data.at.ATStateData;
import org.qortal.data.block.BlockData;
import org.qortal.data.transaction.TransactionData;
@@ -16,27 +12,15 @@ import org.qortal.transform.TransformationException;
import org.qortal.transform.block.BlockTransformer;
import org.qortal.utils.Triple;
-import com.google.common.primitives.Ints;
-
public class BlockMessage extends Message {
private static final Logger LOGGER = LogManager.getLogger(BlockMessage.class);
- private Block block = null;
+ private final BlockData blockData;
+ private final List transactions;
+ private final List atStates;
- private BlockData blockData = null;
- private List transactions = null;
- private List atStates = null;
-
- private int height;
-
- public BlockMessage(Block block) {
- super(MessageType.BLOCK);
-
- this.block = block;
- this.blockData = block.getBlockData();
- this.height = block.getBlockData().getHeight();
- }
+ // No public constructor as we're an incoming-only message type.
private BlockMessage(int id, BlockData blockData, List transactions, List atStates) {
super(id, MessageType.BLOCK);
@@ -44,8 +28,6 @@ public class BlockMessage extends Message {
this.blockData = blockData;
this.transactions = transactions;
this.atStates = atStates;
-
- this.height = blockData.getHeight();
}
public BlockData getBlockData() {
@@ -60,7 +42,7 @@ public class BlockMessage extends Message {
return this.atStates;
}
- public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws UnsupportedEncodingException {
+ public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws MessageException {
try {
int height = byteBuffer.getInt();
@@ -72,32 +54,8 @@ public class BlockMessage extends Message {
return new BlockMessage(id, blockData, blockInfo.getB(), blockInfo.getC());
} catch (TransformationException e) {
LOGGER.info(String.format("Received garbled BLOCK message: %s", e.getMessage()));
- return null;
+ throw new MessageException(e.getMessage(), e);
}
}
- @Override
- protected byte[] toData() {
- if (this.block == null)
- return null;
-
- try {
- ByteArrayOutputStream bytes = new ByteArrayOutputStream();
-
- bytes.write(Ints.toByteArray(this.height));
-
- bytes.write(BlockTransformer.toBytes(this.block));
-
- return bytes.toByteArray();
- } catch (TransformationException | IOException e) {
- return null;
- }
- }
-
- public BlockMessage cloneWithNewId(int newId) {
- BlockMessage clone = new BlockMessage(this.block);
- clone.setId(newId);
- return clone;
- }
-
}
diff --git a/src/main/java/org/qortal/network/message/BlockSummariesMessage.java b/src/main/java/org/qortal/network/message/BlockSummariesMessage.java
index 6a30608b..513e30ae 100644
--- a/src/main/java/org/qortal/network/message/BlockSummariesMessage.java
+++ b/src/main/java/org/qortal/network/message/BlockSummariesMessage.java
@@ -2,7 +2,7 @@ package org.qortal.network.message;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.io.UnsupportedEncodingException;
+import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
@@ -20,7 +20,25 @@ public class BlockSummariesMessage extends Message {
private List blockSummaries;
public BlockSummariesMessage(List blockSummaries) {
- this(-1, blockSummaries);
+ super(MessageType.BLOCK_SUMMARIES);
+
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+
+ try {
+ bytes.write(Ints.toByteArray(blockSummaries.size()));
+
+ for (BlockSummaryData blockSummary : blockSummaries) {
+ bytes.write(Ints.toByteArray(blockSummary.getHeight()));
+ bytes.write(blockSummary.getSignature());
+ bytes.write(blockSummary.getMinterPublicKey());
+ bytes.write(Ints.toByteArray(blockSummary.getOnlineAccountsCount()));
+ }
+ } catch (IOException e) {
+ throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
+ }
+
+ this.dataBytes = bytes.toByteArray();
+ this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private BlockSummariesMessage(int id, List blockSummaries) {
@@ -33,11 +51,11 @@ public class BlockSummariesMessage extends Message {
return this.blockSummaries;
}
- public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
+ public static Message fromByteBuffer(int id, ByteBuffer bytes) {
int count = bytes.getInt();
- if (bytes.remaining() != count * BLOCK_SUMMARY_LENGTH)
- return null;
+ if (bytes.remaining() < count * BLOCK_SUMMARY_LENGTH)
+ throw new BufferUnderflowException();
List blockSummaries = new ArrayList<>();
for (int i = 0; i < count; ++i) {
@@ -58,24 +76,4 @@ public class BlockSummariesMessage extends Message {
return new BlockSummariesMessage(id, blockSummaries);
}
- @Override
- protected byte[] toData() {
- try {
- ByteArrayOutputStream bytes = new ByteArrayOutputStream();
-
- bytes.write(Ints.toByteArray(this.blockSummaries.size()));
-
- for (BlockSummaryData blockSummary : this.blockSummaries) {
- bytes.write(Ints.toByteArray(blockSummary.getHeight()));
- bytes.write(blockSummary.getSignature());
- bytes.write(blockSummary.getMinterPublicKey());
- bytes.write(Ints.toByteArray(blockSummary.getOnlineAccountsCount()));
- }
-
- return bytes.toByteArray();
- } catch (IOException e) {
- return null;
- }
- }
-
}
diff --git a/src/main/java/org/qortal/network/message/CachedBlockMessage.java b/src/main/java/org/qortal/network/message/CachedBlockMessage.java
index e5029ab0..48e9ef36 100644
--- a/src/main/java/org/qortal/network/message/CachedBlockMessage.java
+++ b/src/main/java/org/qortal/network/message/CachedBlockMessage.java
@@ -2,7 +2,6 @@ package org.qortal.network.message;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import org.qortal.block.Block;
@@ -12,59 +11,34 @@ import org.qortal.transform.block.BlockTransformer;
import com.google.common.primitives.Ints;
// This is an OUTGOING-only Message which more readily lends itself to being cached
-public class CachedBlockMessage extends Message {
+public class CachedBlockMessage extends Message implements Cloneable {
- private Block block = null;
- private byte[] cachedBytes = null;
-
- public CachedBlockMessage(Block block) {
+ public CachedBlockMessage(Block block) throws TransformationException {
super(MessageType.BLOCK);
- this.block = block;
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+
+ try {
+ bytes.write(Ints.toByteArray(block.getBlockData().getHeight()));
+
+ bytes.write(BlockTransformer.toBytes(block));
+ } catch (IOException e) {
+ throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
+ }
+
+ this.dataBytes = bytes.toByteArray();
+ this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
public CachedBlockMessage(byte[] cachedBytes) {
super(MessageType.BLOCK);
- this.block = null;
- this.cachedBytes = cachedBytes;
+ this.dataBytes = cachedBytes;
+ this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
-
- public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws UnsupportedEncodingException {
+
+ public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) {
throw new UnsupportedOperationException("CachedBlockMessage is for outgoing messages only");
}
- @Override
- protected byte[] toData() {
- // Already serialized?
- if (this.cachedBytes != null)
- return cachedBytes;
-
- if (this.block == null)
- return null;
-
- try {
- ByteArrayOutputStream bytes = new ByteArrayOutputStream();
-
- bytes.write(Ints.toByteArray(this.block.getBlockData().getHeight()));
-
- bytes.write(BlockTransformer.toBytes(this.block));
-
- this.cachedBytes = bytes.toByteArray();
- // We no longer need source Block
- // and Block contains repository handle which is highly likely to be invalid after this call
- this.block = null;
-
- return this.cachedBytes;
- } catch (TransformationException | IOException e) {
- return null;
- }
- }
-
- public CachedBlockMessage cloneWithNewId(int newId) {
- CachedBlockMessage clone = new CachedBlockMessage(this.cachedBytes);
- clone.setId(newId);
- return clone;
- }
-
}
diff --git a/src/main/java/org/qortal/network/message/ChallengeMessage.java b/src/main/java/org/qortal/network/message/ChallengeMessage.java
index 425f9790..bb5b2ae9 100644
--- a/src/main/java/org/qortal/network/message/ChallengeMessage.java
+++ b/src/main/java/org/qortal/network/message/ChallengeMessage.java
@@ -10,8 +10,25 @@ public class ChallengeMessage extends Message {
public static final int CHALLENGE_LENGTH = 32;
- private final byte[] publicKey;
- private final byte[] challenge;
+ private byte[] publicKey;
+ private byte[] challenge;
+
+ public ChallengeMessage(byte[] publicKey, byte[] challenge) {
+ super(MessageType.CHALLENGE);
+
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream(publicKey.length + challenge.length);
+
+ try {
+ bytes.write(publicKey);
+
+ bytes.write(challenge);
+ } catch (IOException e) {
+ throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
+ }
+
+ this.dataBytes = bytes.toByteArray();
+ this.checksumBytes = Message.generateChecksum(this.dataBytes);
+ }
private ChallengeMessage(int id, byte[] publicKey, byte[] challenge) {
super(id, MessageType.CHALLENGE);
@@ -20,10 +37,6 @@ public class ChallengeMessage extends Message {
this.challenge = challenge;
}
- public ChallengeMessage(byte[] publicKey, byte[] challenge) {
- this(-1, publicKey, challenge);
- }
-
public byte[] getPublicKey() {
return this.publicKey;
}
@@ -42,15 +55,4 @@ public class ChallengeMessage extends Message {
return new ChallengeMessage(id, publicKey, challenge);
}
- @Override
- protected byte[] toData() throws IOException {
- ByteArrayOutputStream bytes = new ByteArrayOutputStream();
-
- bytes.write(this.publicKey);
-
- bytes.write(this.challenge);
-
- return bytes.toByteArray();
- }
-
}
diff --git a/src/main/java/org/qortal/network/message/GetArbitraryDataFileListMessage.java b/src/main/java/org/qortal/network/message/GetArbitraryDataFileListMessage.java
index 542854a5..467a229f 100644
--- a/src/main/java/org/qortal/network/message/GetArbitraryDataFileListMessage.java
+++ b/src/main/java/org/qortal/network/message/GetArbitraryDataFileListMessage.java
@@ -5,33 +5,54 @@ import com.google.common.primitives.Longs;
import org.qortal.data.network.PeerData;
import org.qortal.transform.TransformationException;
import org.qortal.transform.Transformer;
-import org.qortal.transform.transaction.TransactionTransformer;
import org.qortal.utils.Serialization;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
-import static org.qortal.transform.Transformer.INT_LENGTH;
-import static org.qortal.transform.Transformer.LONG_LENGTH;
-
public class GetArbitraryDataFileListMessage extends Message {
- private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
- private static final int HASH_LENGTH = TransactionTransformer.SHA256_LENGTH;
- private static final int MAX_PEER_ADDRESS_LENGTH = PeerData.MAX_PEER_ADDRESS_SIZE;
-
- private final byte[] signature;
+ private byte[] signature;
private List hashes;
- private final long requestTime;
+ private long requestTime;
private int requestHops;
private String requestingPeer;
public GetArbitraryDataFileListMessage(byte[] signature, List hashes, long requestTime, int requestHops, String requestingPeer) {
- this(-1, signature, hashes, requestTime, requestHops, requestingPeer);
+ super(MessageType.GET_ARBITRARY_DATA_FILE_LIST);
+
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+
+ try {
+ bytes.write(signature);
+
+ bytes.write(Longs.toByteArray(requestTime));
+
+ bytes.write(Ints.toByteArray(requestHops));
+
+ if (hashes != null) {
+ bytes.write(Ints.toByteArray(hashes.size()));
+
+ for (byte[] hash : hashes) {
+ bytes.write(hash);
+ }
+ }
+ else {
+ bytes.write(Ints.toByteArray(0));
+ }
+
+ if (requestingPeer != null) {
+ Serialization.serializeSizedStringV2(bytes, requestingPeer);
+ }
+ } catch (IOException e) {
+ throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
+ }
+
+ this.dataBytes = bytes.toByteArray();
+ this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private GetArbitraryDataFileListMessage(int id, byte[] signature, List hashes, long requestTime, int requestHops, String requestingPeer) {
@@ -52,8 +73,20 @@ public class GetArbitraryDataFileListMessage extends Message {
return this.hashes;
}
- public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException, TransformationException {
- byte[] signature = new byte[SIGNATURE_LENGTH];
+ public long getRequestTime() {
+ return this.requestTime;
+ }
+
+ public int getRequestHops() {
+ return this.requestHops;
+ }
+
+ public String getRequestingPeer() {
+ return this.requestingPeer;
+ }
+
+ public static Message fromByteBuffer(int id, ByteBuffer bytes) throws MessageException {
+ byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
bytes.get(signature);
@@ -67,7 +100,7 @@ public class GetArbitraryDataFileListMessage extends Message {
hashes = new ArrayList<>();
for (int i = 0; i < hashCount; ++i) {
- byte[] hash = new byte[HASH_LENGTH];
+ byte[] hash = new byte[Transformer.SHA256_LENGTH];
bytes.get(hash);
hashes.add(hash);
}
@@ -75,57 +108,14 @@ public class GetArbitraryDataFileListMessage extends Message {
String requestingPeer = null;
if (bytes.hasRemaining()) {
- requestingPeer = Serialization.deserializeSizedStringV2(bytes, MAX_PEER_ADDRESS_LENGTH);
+ try {
+ requestingPeer = Serialization.deserializeSizedStringV2(bytes, PeerData.MAX_PEER_ADDRESS_SIZE);
+ } catch (TransformationException e) {
+ throw new MessageException(e.getMessage(), e);
+ }
}
return new GetArbitraryDataFileListMessage(id, signature, hashes, requestTime, requestHops, requestingPeer);
}
- @Override
- protected byte[] toData() {
- try {
- ByteArrayOutputStream bytes = new ByteArrayOutputStream();
-
- bytes.write(this.signature);
-
- bytes.write(Longs.toByteArray(this.requestTime));
-
- bytes.write(Ints.toByteArray(this.requestHops));
-
- if (this.hashes != null) {
- bytes.write(Ints.toByteArray(this.hashes.size()));
-
- for (byte[] hash : this.hashes) {
- bytes.write(hash);
- }
- }
- else {
- bytes.write(Ints.toByteArray(0));
- }
-
- if (this.requestingPeer != null) {
- Serialization.serializeSizedStringV2(bytes, this.requestingPeer);
- }
-
- return bytes.toByteArray();
- } catch (IOException e) {
- return null;
- }
- }
-
- public long getRequestTime() {
- return this.requestTime;
- }
-
- public int getRequestHops() {
- return this.requestHops;
- }
- public void setRequestHops(int requestHops) {
- this.requestHops = requestHops;
- }
-
- public String getRequestingPeer() {
- return this.requestingPeer;
- }
-
}
diff --git a/src/main/java/org/qortal/network/message/GetArbitraryDataFileMessage.java b/src/main/java/org/qortal/network/message/GetArbitraryDataFileMessage.java
index 809b983d..d97a4847 100644
--- a/src/main/java/org/qortal/network/message/GetArbitraryDataFileMessage.java
+++ b/src/main/java/org/qortal/network/message/GetArbitraryDataFileMessage.java
@@ -1,23 +1,31 @@
package org.qortal.network.message;
import org.qortal.transform.Transformer;
-import org.qortal.transform.transaction.TransactionTransformer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
public class GetArbitraryDataFileMessage extends Message {
- private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
- private static final int HASH_LENGTH = TransactionTransformer.SHA256_LENGTH;
-
- private final byte[] signature;
- private final byte[] hash;
+ private byte[] signature;
+ private byte[] hash;
public GetArbitraryDataFileMessage(byte[] signature, byte[] hash) {
- this(-1, signature, hash);
+ super(MessageType.GET_ARBITRARY_DATA_FILE);
+
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream(signature.length + hash.length);
+
+ try {
+ bytes.write(signature);
+
+ bytes.write(hash);
+ } catch (IOException e) {
+ throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
+ }
+
+ this.dataBytes = bytes.toByteArray();
+ this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private GetArbitraryDataFileMessage(int id, byte[] signature, byte[] hash) {
@@ -35,32 +43,14 @@ public class GetArbitraryDataFileMessage extends Message {
return this.hash;
}
- public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
- if (bytes.remaining() != HASH_LENGTH + SIGNATURE_LENGTH)
- return null;
-
- byte[] signature = new byte[SIGNATURE_LENGTH];
+ public static Message fromByteBuffer(int id, ByteBuffer bytes) {
+ byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
bytes.get(signature);
- byte[] hash = new byte[HASH_LENGTH];
+ byte[] hash = new byte[Transformer.SHA256_LENGTH];
bytes.get(hash);
return new GetArbitraryDataFileMessage(id, signature, hash);
}
- @Override
- protected byte[] toData() {
- try {
- ByteArrayOutputStream bytes = new ByteArrayOutputStream();
-
- bytes.write(this.signature);
-
- bytes.write(this.hash);
-
- return bytes.toByteArray();
- } catch (IOException e) {
- return null;
- }
- }
-
}
diff --git a/src/main/java/org/qortal/network/message/GetArbitraryDataMessage.java b/src/main/java/org/qortal/network/message/GetArbitraryDataMessage.java
index 689d704b..bf604fe7 100644
--- a/src/main/java/org/qortal/network/message/GetArbitraryDataMessage.java
+++ b/src/main/java/org/qortal/network/message/GetArbitraryDataMessage.java
@@ -1,20 +1,19 @@
package org.qortal.network.message;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
+import java.util.Arrays;
import org.qortal.transform.Transformer;
public class GetArbitraryDataMessage extends Message {
- private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
-
private byte[] signature;
public GetArbitraryDataMessage(byte[] signature) {
- this(-1, signature);
+ super(MessageType.GET_ARBITRARY_DATA);
+
+ this.dataBytes = Arrays.copyOf(signature, signature.length);
+ this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private GetArbitraryDataMessage(int id, byte[] signature) {
@@ -27,28 +26,12 @@ public class GetArbitraryDataMessage extends Message {
return this.signature;
}
- public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
- if (bytes.remaining() != SIGNATURE_LENGTH)
- return null;
-
- byte[] signature = new byte[SIGNATURE_LENGTH];
+ public static Message fromByteBuffer(int id, ByteBuffer bytes) {
+ byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
bytes.get(signature);
return new GetArbitraryDataMessage(id, signature);
}
- @Override
- protected byte[] toData() {
- try {
- ByteArrayOutputStream bytes = new ByteArrayOutputStream();
-
- bytes.write(this.signature);
-
- return bytes.toByteArray();
- } catch (IOException e) {
- return null;
- }
- }
-
}
diff --git a/src/main/java/org/qortal/network/message/GetArbitraryMetadataMessage.java b/src/main/java/org/qortal/network/message/GetArbitraryMetadataMessage.java
index 66c8f86c..2501d5c3 100644
--- a/src/main/java/org/qortal/network/message/GetArbitraryMetadataMessage.java
+++ b/src/main/java/org/qortal/network/message/GetArbitraryMetadataMessage.java
@@ -6,22 +6,31 @@ import org.qortal.transform.Transformer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
-import static org.qortal.transform.Transformer.INT_LENGTH;
-import static org.qortal.transform.Transformer.LONG_LENGTH;
-
public class GetArbitraryMetadataMessage extends Message {
- private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
-
- private final byte[] signature;
- private final long requestTime;
+ private byte[] signature;
+ private long requestTime;
private int requestHops;
public GetArbitraryMetadataMessage(byte[] signature, long requestTime, int requestHops) {
- this(-1, signature, requestTime, requestHops);
+ super(MessageType.GET_ARBITRARY_METADATA);
+
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+
+ try {
+ bytes.write(signature);
+
+ bytes.write(Longs.toByteArray(requestTime));
+
+ bytes.write(Ints.toByteArray(requestHops));
+ } catch (IOException e) {
+ throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
+ }
+
+ this.dataBytes = bytes.toByteArray();
+ this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private GetArbitraryMetadataMessage(int id, byte[] signature, long requestTime, int requestHops) {
@@ -36,12 +45,16 @@ public class GetArbitraryMetadataMessage extends Message {
return this.signature;
}
- public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
- if (bytes.remaining() != SIGNATURE_LENGTH + LONG_LENGTH + INT_LENGTH)
- return null;
+ public long getRequestTime() {
+ return this.requestTime;
+ }
- byte[] signature = new byte[SIGNATURE_LENGTH];
+ public int getRequestHops() {
+ return this.requestHops;
+ }
+ public static Message fromByteBuffer(int id, ByteBuffer bytes) {
+ byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
bytes.get(signature);
long requestTime = bytes.getLong();
@@ -51,33 +64,4 @@ public class GetArbitraryMetadataMessage extends Message {
return new GetArbitraryMetadataMessage(id, signature, requestTime, requestHops);
}
- @Override
- protected byte[] toData() {
- try {
- ByteArrayOutputStream bytes = new ByteArrayOutputStream();
-
- bytes.write(this.signature);
-
- bytes.write(Longs.toByteArray(this.requestTime));
-
- bytes.write(Ints.toByteArray(this.requestHops));
-
- return bytes.toByteArray();
- } catch (IOException e) {
- return null;
- }
- }
-
- public long getRequestTime() {
- return this.requestTime;
- }
-
- public int getRequestHops() {
- return this.requestHops;
- }
-
- public void setRequestHops(int requestHops) {
- this.requestHops = requestHops;
- }
-
}
diff --git a/src/main/java/org/qortal/network/message/GetBlockMessage.java b/src/main/java/org/qortal/network/message/GetBlockMessage.java
index 43484e69..d39dcca0 100644
--- a/src/main/java/org/qortal/network/message/GetBlockMessage.java
+++ b/src/main/java/org/qortal/network/message/GetBlockMessage.java
@@ -1,20 +1,19 @@
package org.qortal.network.message;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
+import java.util.Arrays;
import org.qortal.transform.block.BlockTransformer;
public class GetBlockMessage extends Message {
- private static final int BLOCK_SIGNATURE_LENGTH = BlockTransformer.BLOCK_SIGNATURE_LENGTH;
-
private byte[] signature;
public GetBlockMessage(byte[] signature) {
- this(-1, signature);
+ super(MessageType.GET_BLOCK);
+
+ this.dataBytes = Arrays.copyOf(signature, signature.length);
+ this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private GetBlockMessage(int id, byte[] signature) {
@@ -27,28 +26,11 @@ public class GetBlockMessage extends Message {
return this.signature;
}
- public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
- if (bytes.remaining() != BLOCK_SIGNATURE_LENGTH)
- return null;
-
- byte[] signature = new byte[BLOCK_SIGNATURE_LENGTH];
-
+ public static Message fromByteBuffer(int id, ByteBuffer bytes) {
+ byte[] signature = new byte[BlockTransformer.BLOCK_SIGNATURE_LENGTH];
bytes.get(signature);
return new GetBlockMessage(id, signature);
}
- @Override
- protected byte[] toData() {
- try {
- ByteArrayOutputStream bytes = new ByteArrayOutputStream();
-
- bytes.write(this.signature);
-
- return bytes.toByteArray();
- } catch (IOException e) {
- return null;
- }
- }
-
}
diff --git a/src/main/java/org/qortal/network/message/GetBlockSummariesMessage.java b/src/main/java/org/qortal/network/message/GetBlockSummariesMessage.java
index 148640fd..70f0d5c5 100644
--- a/src/main/java/org/qortal/network/message/GetBlockSummariesMessage.java
+++ b/src/main/java/org/qortal/network/message/GetBlockSummariesMessage.java
@@ -2,23 +2,32 @@ package org.qortal.network.message;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
-import org.qortal.transform.Transformer;
import org.qortal.transform.block.BlockTransformer;
import com.google.common.primitives.Ints;
public class GetBlockSummariesMessage extends Message {
- private static final int BLOCK_SIGNATURE_LENGTH = BlockTransformer.BLOCK_SIGNATURE_LENGTH;
-
private byte[] parentSignature;
private int numberRequested;
public GetBlockSummariesMessage(byte[] parentSignature, int numberRequested) {
- this(-1, parentSignature, numberRequested);
+ super(MessageType.GET_BLOCK_SUMMARIES);
+
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+
+ try {
+ bytes.write(parentSignature);
+
+ bytes.write(Ints.toByteArray(numberRequested));
+ } catch (IOException e) {
+ throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
+ }
+
+ this.dataBytes = bytes.toByteArray();
+ this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private GetBlockSummariesMessage(int id, byte[] parentSignature, int numberRequested) {
@@ -36,11 +45,8 @@ public class GetBlockSummariesMessage extends Message {
return this.numberRequested;
}
- public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
- if (bytes.remaining() != BLOCK_SIGNATURE_LENGTH + Transformer.INT_LENGTH)
- return null;
-
- byte[] parentSignature = new byte[BLOCK_SIGNATURE_LENGTH];
+ public static Message fromByteBuffer(int id, ByteBuffer bytes) {
+ byte[] parentSignature = new byte[BlockTransformer.BLOCK_SIGNATURE_LENGTH];
bytes.get(parentSignature);
int numberRequested = bytes.getInt();
@@ -48,19 +54,4 @@ public class GetBlockSummariesMessage extends Message {
return new GetBlockSummariesMessage(id, parentSignature, numberRequested);
}
- @Override
- protected byte[] toData() {
- try {
- ByteArrayOutputStream bytes = new ByteArrayOutputStream();
-
- bytes.write(this.parentSignature);
-
- bytes.write(Ints.toByteArray(this.numberRequested));
-
- return bytes.toByteArray();
- } catch (IOException e) {
- return null;
- }
- }
-
}
diff --git a/src/main/java/org/qortal/network/message/GetOnlineAccountsV2Message.java b/src/main/java/org/qortal/network/message/GetOnlineAccountsV2Message.java
index 709f9782..fe6b5d72 100644
--- a/src/main/java/org/qortal/network/message/GetOnlineAccountsV2Message.java
+++ b/src/main/java/org/qortal/network/message/GetOnlineAccountsV2Message.java
@@ -7,7 +7,6 @@ import org.qortal.transform.Transformer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
@@ -24,11 +23,51 @@ import java.util.Map;
* Also V2 only builds online accounts message once!
*/
public class GetOnlineAccountsV2Message extends Message {
+
private List onlineAccounts;
- private byte[] cachedData;
public GetOnlineAccountsV2Message(List onlineAccounts) {
- this(-1, onlineAccounts);
+ super(MessageType.GET_ONLINE_ACCOUNTS_V2);
+
+ // If we don't have ANY online accounts then it's an easier construction...
+ if (onlineAccounts.isEmpty()) {
+ // Always supply a number of accounts
+ this.dataBytes = Ints.toByteArray(0);
+ this.checksumBytes = Message.generateChecksum(this.dataBytes);
+ return;
+ }
+
+ // How many of each timestamp
+ Map countByTimestamp = new HashMap<>();
+
+ for (OnlineAccountData onlineAccountData : onlineAccounts) {
+ Long timestamp = onlineAccountData.getTimestamp();
+ countByTimestamp.compute(timestamp, (k, v) -> v == null ? 1 : ++v);
+ }
+
+ // We should know exactly how many bytes to allocate now
+ int byteSize = countByTimestamp.size() * (Transformer.INT_LENGTH + Transformer.TIMESTAMP_LENGTH)
+ + onlineAccounts.size() * Transformer.PUBLIC_KEY_LENGTH;
+
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream(byteSize);
+
+ try {
+ for (long timestamp : countByTimestamp.keySet()) {
+ bytes.write(Ints.toByteArray(countByTimestamp.get(timestamp)));
+
+ bytes.write(Longs.toByteArray(timestamp));
+
+ for (OnlineAccountData onlineAccountData : onlineAccounts) {
+ if (onlineAccountData.getTimestamp() == timestamp)
+ bytes.write(onlineAccountData.getPublicKey());
+ }
+ }
+ } catch (IOException e) {
+ throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
+ }
+
+ this.dataBytes = bytes.toByteArray();
+ this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private GetOnlineAccountsV2Message(int id, List onlineAccounts) {
@@ -41,7 +80,7 @@ public class GetOnlineAccountsV2Message extends Message {
return this.onlineAccounts;
}
- public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
+ public static Message fromByteBuffer(int id, ByteBuffer bytes) {
int accountCount = bytes.getInt();
List onlineAccounts = new ArrayList<>(accountCount);
@@ -67,51 +106,4 @@ public class GetOnlineAccountsV2Message extends Message {
return new GetOnlineAccountsV2Message(id, onlineAccounts);
}
- @Override
- protected synchronized byte[] toData() {
- if (this.cachedData != null)
- return this.cachedData;
-
- // Shortcut in case we have no online accounts
- if (this.onlineAccounts.isEmpty()) {
- this.cachedData = Ints.toByteArray(0);
- return this.cachedData;
- }
-
- // How many of each timestamp
- Map countByTimestamp = new HashMap<>();
-
- for (int i = 0; i < this.onlineAccounts.size(); ++i) {
- OnlineAccountData onlineAccountData = this.onlineAccounts.get(i);
- Long timestamp = onlineAccountData.getTimestamp();
- countByTimestamp.compute(timestamp, (k, v) -> v == null ? 1 : ++v);
- }
-
- // We should know exactly how many bytes to allocate now
- int byteSize = countByTimestamp.size() * (Transformer.INT_LENGTH + Transformer.TIMESTAMP_LENGTH)
- + this.onlineAccounts.size() * Transformer.PUBLIC_KEY_LENGTH;
-
- try {
- ByteArrayOutputStream bytes = new ByteArrayOutputStream(byteSize);
-
- for (long timestamp : countByTimestamp.keySet()) {
- bytes.write(Ints.toByteArray(countByTimestamp.get(timestamp)));
-
- bytes.write(Longs.toByteArray(timestamp));
-
- for (int i = 0; i < this.onlineAccounts.size(); ++i) {
- OnlineAccountData onlineAccountData = this.onlineAccounts.get(i);
-
- if (onlineAccountData.getTimestamp() == timestamp)
- bytes.write(onlineAccountData.getPublicKey());
- }
- }
-
- this.cachedData = bytes.toByteArray();
- return this.cachedData;
- } catch (IOException e) {
- return null;
- }
- }
-
}
diff --git a/src/main/java/org/qortal/network/message/GetPeersMessage.java b/src/main/java/org/qortal/network/message/GetPeersMessage.java
index 21b06df5..b8f7e128 100644
--- a/src/main/java/org/qortal/network/message/GetPeersMessage.java
+++ b/src/main/java/org/qortal/network/message/GetPeersMessage.java
@@ -1,25 +1,21 @@
package org.qortal.network.message;
-import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
public class GetPeersMessage extends Message {
public GetPeersMessage() {
- this(-1);
+ super(MessageType.GET_PEERS);
+
+ this.dataBytes = EMPTY_DATA_BYTES;
}
private GetPeersMessage(int id) {
super(id, MessageType.GET_PEERS);
}
- public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
+ public static Message fromByteBuffer(int id, ByteBuffer bytes) {
return new GetPeersMessage(id);
}
- @Override
- protected byte[] toData() {
- return new byte[0];
- }
-
}
diff --git a/src/main/java/org/qortal/network/message/GetSignaturesV2Message.java b/src/main/java/org/qortal/network/message/GetSignaturesV2Message.java
index 2dc54365..0f88ba7d 100644
--- a/src/main/java/org/qortal/network/message/GetSignaturesV2Message.java
+++ b/src/main/java/org/qortal/network/message/GetSignaturesV2Message.java
@@ -2,24 +2,32 @@ package org.qortal.network.message;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
-import org.qortal.transform.Transformer;
import org.qortal.transform.block.BlockTransformer;
import com.google.common.primitives.Ints;
public class GetSignaturesV2Message extends Message {
- private static final int BLOCK_SIGNATURE_LENGTH = BlockTransformer.BLOCK_SIGNATURE_LENGTH;
- private static final int NUMBER_REQUESTED_LENGTH = Transformer.INT_LENGTH;
-
private byte[] parentSignature;
private int numberRequested;
public GetSignaturesV2Message(byte[] parentSignature, int numberRequested) {
- this(-1, parentSignature, numberRequested);
+ super(MessageType.GET_SIGNATURES_V2);
+
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+
+ try {
+ bytes.write(parentSignature);
+
+ bytes.write(Ints.toByteArray(numberRequested));
+ } catch (IOException e) {
+ throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
+ }
+
+ this.dataBytes = bytes.toByteArray();
+ this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private GetSignaturesV2Message(int id, byte[] parentSignature, int numberRequested) {
@@ -37,11 +45,8 @@ public class GetSignaturesV2Message extends Message {
return this.numberRequested;
}
- public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
- if (bytes.remaining() != BLOCK_SIGNATURE_LENGTH + NUMBER_REQUESTED_LENGTH)
- return null;
-
- byte[] parentSignature = new byte[BLOCK_SIGNATURE_LENGTH];
+ public static Message fromByteBuffer(int id, ByteBuffer bytes) {
+ byte[] parentSignature = new byte[BlockTransformer.BLOCK_SIGNATURE_LENGTH];
bytes.get(parentSignature);
int numberRequested = bytes.getInt();
@@ -49,19 +54,4 @@ public class GetSignaturesV2Message extends Message {
return new GetSignaturesV2Message(id, parentSignature, numberRequested);
}
- @Override
- protected byte[] toData() {
- try {
- ByteArrayOutputStream bytes = new ByteArrayOutputStream();
-
- bytes.write(this.parentSignature);
-
- bytes.write(Ints.toByteArray(this.numberRequested));
-
- return bytes.toByteArray();
- } catch (IOException e) {
- return null;
- }
- }
-
}
diff --git a/src/main/java/org/qortal/network/message/GetTradePresencesMessage.java b/src/main/java/org/qortal/network/message/GetTradePresencesMessage.java
index d9be3c1b..7246c424 100644
--- a/src/main/java/org/qortal/network/message/GetTradePresencesMessage.java
+++ b/src/main/java/org/qortal/network/message/GetTradePresencesMessage.java
@@ -7,7 +7,6 @@ import org.qortal.transform.Transformer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
@@ -21,10 +20,48 @@ import java.util.Map;
*/
public class GetTradePresencesMessage extends Message {
private List tradePresences;
- private byte[] cachedData;
public GetTradePresencesMessage(List tradePresences) {
- this(-1, tradePresences);
+ super(MessageType.GET_TRADE_PRESENCES);
+
+ // Shortcut in case we have no trade presences
+ if (tradePresences.isEmpty()) {
+ this.dataBytes = Ints.toByteArray(0);
+ this.checksumBytes = Message.generateChecksum(this.dataBytes);
+ return;
+ }
+
+ // How many of each timestamp
+ Map countByTimestamp = new HashMap<>();
+
+ for (TradePresenceData tradePresenceData : tradePresences) {
+ Long timestamp = tradePresenceData.getTimestamp();
+ countByTimestamp.compute(timestamp, (k, v) -> v == null ? 1 : ++v);
+ }
+
+ // We should know exactly how many bytes to allocate now
+ int byteSize = countByTimestamp.size() * (Transformer.INT_LENGTH + Transformer.TIMESTAMP_LENGTH)
+ + tradePresences.size() * Transformer.PUBLIC_KEY_LENGTH;
+
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream(byteSize);
+
+ try {
+ for (long timestamp : countByTimestamp.keySet()) {
+ bytes.write(Ints.toByteArray(countByTimestamp.get(timestamp)));
+
+ bytes.write(Longs.toByteArray(timestamp));
+
+ for (TradePresenceData tradePresenceData : tradePresences) {
+ if (tradePresenceData.getTimestamp() == timestamp)
+ bytes.write(tradePresenceData.getPublicKey());
+ }
+ }
+ } catch (IOException e) {
+ throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
+ }
+
+ this.dataBytes = bytes.toByteArray();
+ this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private GetTradePresencesMessage(int id, List tradePresences) {
@@ -37,7 +74,7 @@ public class GetTradePresencesMessage extends Message {
return this.tradePresences;
}
- public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
+ public static Message fromByteBuffer(int id, ByteBuffer bytes) {
int groupedEntriesCount = bytes.getInt();
List tradePresences = new ArrayList<>(groupedEntriesCount);
@@ -63,48 +100,4 @@ public class GetTradePresencesMessage extends Message {
return new GetTradePresencesMessage(id, tradePresences);
}
- @Override
- protected synchronized byte[] toData() {
- if (this.cachedData != null)
- return this.cachedData;
-
- // Shortcut in case we have no trade presences
- if (this.tradePresences.isEmpty()) {
- this.cachedData = Ints.toByteArray(0);
- return this.cachedData;
- }
-
- // How many of each timestamp
- Map countByTimestamp = new HashMap<>();
-
- for (TradePresenceData tradePresenceData : this.tradePresences) {
- Long timestamp = tradePresenceData.getTimestamp();
- countByTimestamp.compute(timestamp, (k, v) -> v == null ? 1 : ++v);
- }
-
- // We should know exactly how many bytes to allocate now
- int byteSize = countByTimestamp.size() * (Transformer.INT_LENGTH + Transformer.TIMESTAMP_LENGTH)
- + this.tradePresences.size() * Transformer.PUBLIC_KEY_LENGTH;
-
- try {
- ByteArrayOutputStream bytes = new ByteArrayOutputStream(byteSize);
-
- for (long timestamp : countByTimestamp.keySet()) {
- bytes.write(Ints.toByteArray(countByTimestamp.get(timestamp)));
-
- bytes.write(Longs.toByteArray(timestamp));
-
- for (TradePresenceData tradePresenceData : this.tradePresences) {
- if (tradePresenceData.getTimestamp() == timestamp)
- bytes.write(tradePresenceData.getPublicKey());
- }
- }
-
- this.cachedData = bytes.toByteArray();
- return this.cachedData;
- } catch (IOException e) {
- return null;
- }
- }
-
}
diff --git a/src/main/java/org/qortal/network/message/GetTransactionMessage.java b/src/main/java/org/qortal/network/message/GetTransactionMessage.java
index 2ea06580..fe0c750f 100644
--- a/src/main/java/org/qortal/network/message/GetTransactionMessage.java
+++ b/src/main/java/org/qortal/network/message/GetTransactionMessage.java
@@ -1,20 +1,19 @@
package org.qortal.network.message;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
+import java.util.Arrays;
import org.qortal.transform.Transformer;
public class GetTransactionMessage extends Message {
- private static final int TRANSACTION_SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
-
private byte[] signature;
public GetTransactionMessage(byte[] signature) {
- this(-1, signature);
+ super(MessageType.GET_TRANSACTION);
+
+ this.dataBytes = Arrays.copyOf(signature, signature.length);
+ this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private GetTransactionMessage(int id, byte[] signature) {
@@ -27,28 +26,12 @@ public class GetTransactionMessage extends Message {
return this.signature;
}
- public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
- if (bytes.remaining() != TRANSACTION_SIGNATURE_LENGTH)
- return null;
-
- byte[] signature = new byte[TRANSACTION_SIGNATURE_LENGTH];
+ public static Message fromByteBuffer(int id, ByteBuffer bytes) {
+ byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
bytes.get(signature);
return new GetTransactionMessage(id, signature);
}
- @Override
- protected byte[] toData() {
- try {
- ByteArrayOutputStream bytes = new ByteArrayOutputStream();
-
- bytes.write(this.signature);
-
- return bytes.toByteArray();
- } catch (IOException e) {
- return null;
- }
- }
-
}
diff --git a/src/main/java/org/qortal/network/message/GetUnconfirmedTransactionsMessage.java b/src/main/java/org/qortal/network/message/GetUnconfirmedTransactionsMessage.java
index 18260568..fccd4c74 100644
--- a/src/main/java/org/qortal/network/message/GetUnconfirmedTransactionsMessage.java
+++ b/src/main/java/org/qortal/network/message/GetUnconfirmedTransactionsMessage.java
@@ -1,25 +1,21 @@
package org.qortal.network.message;
-import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
public class GetUnconfirmedTransactionsMessage extends Message {
public GetUnconfirmedTransactionsMessage() {
- this(-1);
+ super(MessageType.GET_UNCONFIRMED_TRANSACTIONS);
+
+ this.dataBytes = EMPTY_DATA_BYTES;
}
private GetUnconfirmedTransactionsMessage(int id) {
super(id, MessageType.GET_UNCONFIRMED_TRANSACTIONS);
}
- public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
+ public static Message fromByteBuffer(int id, ByteBuffer bytes) {
return new GetUnconfirmedTransactionsMessage(id);
}
- @Override
- protected byte[] toData() {
- return new byte[0];
- }
-
}
diff --git a/src/main/java/org/qortal/network/message/GoodbyeMessage.java b/src/main/java/org/qortal/network/message/GoodbyeMessage.java
index 75864060..74130be2 100644
--- a/src/main/java/org/qortal/network/message/GoodbyeMessage.java
+++ b/src/main/java/org/qortal/network/message/GoodbyeMessage.java
@@ -3,7 +3,6 @@ package org.qortal.network.message;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toMap;
-import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Map;
@@ -22,7 +21,7 @@ public class GoodbyeMessage extends Message {
private static final Map map = stream(Reason.values())
.collect(toMap(reason -> reason.value, reason -> reason));
- private Reason(int value) {
+ Reason(int value) {
this.value = value;
}
@@ -31,7 +30,14 @@ public class GoodbyeMessage extends Message {
}
}
- private final Reason reason;
+ private Reason reason;
+
+ public GoodbyeMessage(Reason reason) {
+ super(MessageType.GOODBYE);
+
+ this.dataBytes = Ints.toByteArray(reason.value);
+ this.checksumBytes = Message.generateChecksum(this.dataBytes);
+ }
private GoodbyeMessage(int id, Reason reason) {
super(id, MessageType.GOODBYE);
@@ -39,27 +45,18 @@ public class GoodbyeMessage extends Message {
this.reason = reason;
}
- public GoodbyeMessage(Reason reason) {
- this(-1, reason);
- }
-
public Reason getReason() {
return this.reason;
}
- public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) {
+ public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws MessageException {
int reasonValue = byteBuffer.getInt();
Reason reason = Reason.valueOf(reasonValue);
if (reason == null)
- return null;
+ throw new MessageException("Invalid reason " + reasonValue + " in GOODBYE message");
return new GoodbyeMessage(id, reason);
}
- @Override
- protected byte[] toData() throws IOException {
- return Ints.toByteArray(this.reason.value);
- }
-
}
diff --git a/src/main/java/org/qortal/network/message/HeightV2Message.java b/src/main/java/org/qortal/network/message/HeightV2Message.java
index 4d6f3f21..0e775a84 100644
--- a/src/main/java/org/qortal/network/message/HeightV2Message.java
+++ b/src/main/java/org/qortal/network/message/HeightV2Message.java
@@ -2,7 +2,6 @@ package org.qortal.network.message;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import org.qortal.transform.Transformer;
@@ -19,7 +18,24 @@ public class HeightV2Message extends Message {
private byte[] minterPublicKey;
public HeightV2Message(int height, byte[] signature, long timestamp, byte[] minterPublicKey) {
- this(-1, height, signature, timestamp, minterPublicKey);
+ super(MessageType.HEIGHT_V2);
+
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+
+ try {
+ bytes.write(Ints.toByteArray(height));
+
+ bytes.write(signature);
+
+ bytes.write(Longs.toByteArray(timestamp));
+
+ bytes.write(minterPublicKey);
+ } catch (IOException e) {
+ throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
+ }
+
+ this.dataBytes = bytes.toByteArray();
+ this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private HeightV2Message(int id, int height, byte[] signature, long timestamp, byte[] minterPublicKey) {
@@ -47,7 +63,7 @@ public class HeightV2Message extends Message {
return this.minterPublicKey;
}
- public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
+ public static Message fromByteBuffer(int id, ByteBuffer bytes) {
int height = bytes.getInt();
byte[] signature = new byte[BlockTransformer.BLOCK_SIGNATURE_LENGTH];
@@ -61,23 +77,4 @@ public class HeightV2Message extends Message {
return new HeightV2Message(id, height, signature, timestamp, minterPublicKey);
}
- @Override
- protected byte[] toData() {
- try {
- ByteArrayOutputStream bytes = new ByteArrayOutputStream();
-
- bytes.write(Ints.toByteArray(this.height));
-
- bytes.write(this.signature);
-
- bytes.write(Longs.toByteArray(this.timestamp));
-
- bytes.write(this.minterPublicKey);
-
- return bytes.toByteArray();
- } catch (IOException e) {
- return null;
- }
- }
-
}
diff --git a/src/main/java/org/qortal/network/message/HelloMessage.java b/src/main/java/org/qortal/network/message/HelloMessage.java
index 1b6de17d..30b7d9be 100644
--- a/src/main/java/org/qortal/network/message/HelloMessage.java
+++ b/src/main/java/org/qortal/network/message/HelloMessage.java
@@ -11,9 +11,28 @@ import com.google.common.primitives.Longs;
public class HelloMessage extends Message {
- private final long timestamp;
- private final String versionString;
- private final String senderPeerAddress;
+ private long timestamp;
+ private String versionString;
+ private String senderPeerAddress;
+
+ public HelloMessage(long timestamp, String versionString, String senderPeerAddress) {
+ super(MessageType.HELLO);
+
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+
+ try {
+ bytes.write(Longs.toByteArray(timestamp));
+
+ Serialization.serializeSizedString(bytes, versionString);
+
+ Serialization.serializeSizedString(bytes, senderPeerAddress);
+ } catch (IOException e) {
+ throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
+ }
+
+ this.dataBytes = bytes.toByteArray();
+ this.checksumBytes = Message.generateChecksum(this.dataBytes);
+ }
private HelloMessage(int id, long timestamp, String versionString, String senderPeerAddress) {
super(id, MessageType.HELLO);
@@ -23,10 +42,6 @@ public class HelloMessage extends Message {
this.senderPeerAddress = senderPeerAddress;
}
- public HelloMessage(long timestamp, String versionString, String senderPeerAddress) {
- this(-1, timestamp, versionString, senderPeerAddress);
- }
-
public long getTimestamp() {
return this.timestamp;
}
@@ -39,31 +54,23 @@ public class HelloMessage extends Message {
return this.senderPeerAddress;
}
- public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws TransformationException {
+ public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws MessageException {
long timestamp = byteBuffer.getLong();
- String versionString = Serialization.deserializeSizedString(byteBuffer, 255);
-
- // Sender peer address added in v3.0, so is an optional field. Older versions won't send it.
+ String versionString;
String senderPeerAddress = null;
- if (byteBuffer.hasRemaining()) {
- senderPeerAddress = Serialization.deserializeSizedString(byteBuffer, 255);
+ try {
+ versionString = Serialization.deserializeSizedString(byteBuffer, 255);
+
+ // Sender peer address added in v3.0, so is an optional field. Older versions won't send it.
+ if (byteBuffer.hasRemaining()) {
+ senderPeerAddress = Serialization.deserializeSizedString(byteBuffer, 255);
+ }
+ } catch (TransformationException e) {
+ throw new MessageException(e.getMessage(), e);
}
return new HelloMessage(id, timestamp, versionString, senderPeerAddress);
}
- @Override
- protected byte[] toData() throws IOException {
- ByteArrayOutputStream bytes = new ByteArrayOutputStream();
-
- bytes.write(Longs.toByteArray(this.timestamp));
-
- Serialization.serializeSizedString(bytes, this.versionString);
-
- Serialization.serializeSizedString(bytes, this.senderPeerAddress);
-
- return bytes.toByteArray();
- }
-
}
diff --git a/src/main/java/org/qortal/network/message/Message.java b/src/main/java/org/qortal/network/message/Message.java
index 094c1143..e92aca89 100644
--- a/src/main/java/org/qortal/network/message/Message.java
+++ b/src/main/java/org/qortal/network/message/Message.java
@@ -1,160 +1,67 @@
package org.qortal.network.message;
-import java.util.Map;
-
import org.qortal.crypto.Crypto;
import org.qortal.network.Network;
-import org.qortal.transform.TransformationException;
import com.google.common.primitives.Ints;
-import static java.util.Arrays.stream;
-import static java.util.stream.Collectors.toMap;
-
import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.Arrays;
+/**
+ * Network message for sending over network, or unpacked data received from network.
+ *
+ *
+ * For messages received from network, subclass's {@code fromByteBuffer()} method is used
+ * to construct a subclassed instance. Original bytes from network are not retained.
+ * Access to deserialized data should be via subclass's getters. Ideally there should be NO setters!
+ *
+ *
+ *
+ * Each subclass's public constructor is for building a message to send only.
+ * The constructor will serialize into byte form but not store the passed args.
+ * Serialized bytes are saved into superclass (Message) {@code dataBytes} and, if not empty,
+ * a checksum is created and saved into {@code checksumBytes}.
+ * Therefore: do not use subclass's getters after using constructor!
+ *
+ *
+ *
+ * For subclasses where outgoing versions might be usefully cached, they can implement Clonable
+ * as long if they are safe to use {@link Object#clone()}.
+ *
+ */
public abstract class Message {
// MAGIC(4) + TYPE(4) + HAS-ID(1) + ID?(4) + DATA-SIZE(4) + CHECKSUM?(4) + DATA?(*)
private static final int MAGIC_LENGTH = 4;
+ private static final int TYPE_LENGTH = 4;
+ private static final int HAS_ID_LENGTH = 1;
+ private static final int ID_LENGTH = 4;
+ private static final int DATA_SIZE_LENGTH = 4;
private static final int CHECKSUM_LENGTH = 4;
private static final int MAX_DATA_SIZE = 10 * 1024 * 1024; // 10MB
- @SuppressWarnings("serial")
- public static class MessageException extends Exception {
- public MessageException() {
- }
+ protected static final byte[] EMPTY_DATA_BYTES = new byte[0];
- public MessageException(String message) {
- super(message);
- }
+ protected int id;
+ protected final MessageType type;
- public MessageException(String message, Throwable cause) {
- super(message, cause);
- }
-
- public MessageException(Throwable cause) {
- super(cause);
- }
- }
-
- public enum MessageType {
- // Handshaking
- HELLO(0),
- GOODBYE(1),
- CHALLENGE(2),
- RESPONSE(3),
-
- // Status / notifications
- HEIGHT_V2(10),
- PING(11),
- PONG(12),
-
- // Requesting data
- PEERS_V2(20),
- GET_PEERS(21),
-
- TRANSACTION(30),
- GET_TRANSACTION(31),
-
- TRANSACTION_SIGNATURES(40),
- GET_UNCONFIRMED_TRANSACTIONS(41),
-
- BLOCK(50),
- GET_BLOCK(51),
-
- SIGNATURES(60),
- GET_SIGNATURES_V2(61),
-
- BLOCK_SUMMARIES(70),
- GET_BLOCK_SUMMARIES(71),
-
- ONLINE_ACCOUNTS_V2(82),
- GET_ONLINE_ACCOUNTS_V2(83),
- ONLINE_ACCOUNTS_V3(84),
-
- ARBITRARY_DATA(90),
- GET_ARBITRARY_DATA(91),
-
- BLOCKS(100),
- GET_BLOCKS(101),
-
- ARBITRARY_DATA_FILE(110),
- GET_ARBITRARY_DATA_FILE(111),
-
- ARBITRARY_DATA_FILE_LIST(120),
- GET_ARBITRARY_DATA_FILE_LIST(121),
-
- ARBITRARY_SIGNATURES(130),
-
- TRADE_PRESENCES(140),
- GET_TRADE_PRESENCES(141),
-
- ARBITRARY_METADATA(150),
- GET_ARBITRARY_METADATA(151);
-
- public final int value;
- public final Method fromByteBufferMethod;
-
- private static final Map map = stream(MessageType.values())
- .collect(toMap(messageType -> messageType.value, messageType -> messageType));
-
- private MessageType(int value) {
- this.value = value;
-
- String[] classNameParts = this.name().toLowerCase().split("_");
-
- for (int i = 0; i < classNameParts.length; ++i)
- classNameParts[i] = classNameParts[i].substring(0, 1).toUpperCase().concat(classNameParts[i].substring(1));
-
- String className = String.join("", classNameParts);
-
- Method method;
- try {
- Class> subclass = Class.forName(String.join("", Message.class.getPackage().getName(), ".", className, "Message"));
-
- method = subclass.getDeclaredMethod("fromByteBuffer", int.class, ByteBuffer.class);
- } catch (ClassNotFoundException | NoSuchMethodException | SecurityException e) {
- method = null;
- }
-
- this.fromByteBufferMethod = method;
- }
-
- public static MessageType valueOf(int value) {
- return map.get(value);
- }
-
- public Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws MessageException {
- if (this.fromByteBufferMethod == null)
- throw new MessageException("Unsupported message type [" + value + "] during conversion from bytes");
-
- try {
- return (Message) this.fromByteBufferMethod.invoke(null, id, byteBuffer);
- } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
- if (e.getCause() instanceof BufferUnderflowException)
- throw new MessageException("Byte data too short for " + name() + " message");
-
- throw new MessageException("Internal error with " + name() + " message during conversion from bytes");
- }
- }
- }
-
- private int id;
- private MessageType type;
+ /** Serialized outgoing message data. Expected to be written to by subclass. */
+ protected byte[] dataBytes;
+ /** Serialized outgoing message checksum. Expected to be written to by subclass. */
+ protected byte[] checksumBytes;
+ /** Typically called by subclass when constructing message from received network data. */
protected Message(int id, MessageType type) {
this.id = id;
this.type = type;
}
+ /** Typically called by subclass when constructing outgoing message. */
protected Message(MessageType type) {
this(-1, type);
}
@@ -178,9 +85,9 @@ public abstract class Message {
/**
* Attempt to read a message from byte buffer.
*
- * @param readOnlyBuffer
+ * @param readOnlyBuffer ByteBuffer containing bytes read from network
* @return null if no complete message can be read
- * @throws MessageException
+ * @throws MessageException if message could not be decoded or is invalid
*/
public static Message fromByteBuffer(ByteBuffer readOnlyBuffer) throws MessageException {
try {
@@ -255,9 +162,27 @@ public abstract class Message {
return Arrays.copyOfRange(Crypto.digest(dataBuffer), 0, CHECKSUM_LENGTH);
}
+ public void checkValidOutgoing() throws MessageException {
+ // We expect subclass to have initialized these
+ if (this.dataBytes == null)
+ throw new MessageException("Missing data payload");
+ if (this.dataBytes.length > 0 && this.checksumBytes == null)
+ throw new MessageException("Missing data checksum");
+ }
+
public byte[] toBytes() throws MessageException {
+ checkValidOutgoing();
+
+ // We can calculate exact length
+ int messageLength = MAGIC_LENGTH + TYPE_LENGTH + HAS_ID_LENGTH;
+ messageLength += this.hasId() ? ID_LENGTH : 0;
+ messageLength += DATA_SIZE_LENGTH + this.dataBytes.length > 0 ? CHECKSUM_LENGTH + this.dataBytes.length : 0;
+
+ if (messageLength > MAX_DATA_SIZE)
+ throw new MessageException(String.format("About to send message with length %d larger than allowed %d", messageLength, MAX_DATA_SIZE));
+
try {
- ByteArrayOutputStream bytes = new ByteArrayOutputStream(256);
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream(messageLength);
// Magic
bytes.write(Network.getInstance().getMessageMagic());
@@ -272,26 +197,30 @@ public abstract class Message {
bytes.write(0);
}
- byte[] data = this.toData();
- if (data == null)
- throw new MessageException("Missing data payload");
+ bytes.write(Ints.toByteArray(this.dataBytes.length));
- bytes.write(Ints.toByteArray(data.length));
-
- if (data.length > 0) {
- bytes.write(generateChecksum(data));
- bytes.write(data);
+ if (this.dataBytes.length > 0) {
+ bytes.write(this.checksumBytes);
+ bytes.write(this.dataBytes);
}
- if (bytes.size() > MAX_DATA_SIZE)
- throw new MessageException(String.format("About to send message with length %d larger than allowed %d", bytes.size(), MAX_DATA_SIZE));
-
return bytes.toByteArray();
- } catch (IOException | TransformationException e) {
+ } catch (IOException e) {
throw new MessageException("Failed to serialize message", e);
}
}
- protected abstract byte[] toData() throws IOException, TransformationException;
+ public static M cloneWithNewId(M message, int newId) {
+ M clone;
+
+ try {
+ clone = (M) message.clone();
+ } catch (CloneNotSupportedException e) {
+ throw new UnsupportedOperationException("Message sub-class not cloneable");
+ }
+
+ clone.setId(newId);
+ return clone;
+ }
}
diff --git a/src/main/java/org/qortal/network/message/MessageException.java b/src/main/java/org/qortal/network/message/MessageException.java
new file mode 100644
index 00000000..97e8d0be
--- /dev/null
+++ b/src/main/java/org/qortal/network/message/MessageException.java
@@ -0,0 +1,19 @@
+package org.qortal.network.message;
+
+@SuppressWarnings("serial")
+public class MessageException extends Exception {
+ public MessageException() {
+ }
+
+ public MessageException(String message) {
+ super(message);
+ }
+
+ public MessageException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public MessageException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/src/main/java/org/qortal/network/message/MessageProducer.java b/src/main/java/org/qortal/network/message/MessageProducer.java
new file mode 100644
index 00000000..7f203788
--- /dev/null
+++ b/src/main/java/org/qortal/network/message/MessageProducer.java
@@ -0,0 +1,8 @@
+package org.qortal.network.message;
+
+import java.nio.ByteBuffer;
+
+@FunctionalInterface
+public interface MessageProducer {
+ Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws MessageException;
+}
diff --git a/src/main/java/org/qortal/network/message/MessageType.java b/src/main/java/org/qortal/network/message/MessageType.java
new file mode 100644
index 00000000..48039a4d
--- /dev/null
+++ b/src/main/java/org/qortal/network/message/MessageType.java
@@ -0,0 +1,96 @@
+package org.qortal.network.message;
+
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.Map;
+
+import static java.util.Arrays.stream;
+import static java.util.stream.Collectors.toMap;
+
+public enum MessageType {
+ // Handshaking
+ HELLO(0, HelloMessage::fromByteBuffer),
+ GOODBYE(1, GoodbyeMessage::fromByteBuffer),
+ CHALLENGE(2, ChallengeMessage::fromByteBuffer),
+ RESPONSE(3, ResponseMessage::fromByteBuffer),
+
+ // Status / notifications
+ HEIGHT_V2(10, HeightV2Message::fromByteBuffer),
+ PING(11, PingMessage::fromByteBuffer),
+ PONG(12, PongMessage::fromByteBuffer),
+
+ // Requesting data
+ PEERS_V2(20, PeersV2Message::fromByteBuffer),
+ GET_PEERS(21, GetPeersMessage::fromByteBuffer),
+
+ TRANSACTION(30, TransactionMessage::fromByteBuffer),
+ GET_TRANSACTION(31, GetTransactionMessage::fromByteBuffer),
+
+ TRANSACTION_SIGNATURES(40, TransactionSignaturesMessage::fromByteBuffer),
+ GET_UNCONFIRMED_TRANSACTIONS(41, GetUnconfirmedTransactionsMessage::fromByteBuffer),
+
+ BLOCK(50, BlockMessage::fromByteBuffer),
+ GET_BLOCK(51, GetBlockMessage::fromByteBuffer),
+
+ SIGNATURES(60, SignaturesMessage::fromByteBuffer),
+ GET_SIGNATURES_V2(61, GetSignaturesV2Message::fromByteBuffer),
+
+ BLOCK_SUMMARIES(70, BlockSummariesMessage::fromByteBuffer),
+ GET_BLOCK_SUMMARIES(71, GetBlockSummariesMessage::fromByteBuffer),
+
+ ONLINE_ACCOUNTS(80, OnlineAccountsMessage::fromByteBuffer),
+ GET_ONLINE_ACCOUNTS(81, GetOnlineAccountsMessage::fromByteBuffer),
+ ONLINE_ACCOUNTS_V2(82, OnlineAccountsV2Message::fromByteBuffer),
+ GET_ONLINE_ACCOUNTS_V2(83, GetOnlineAccountsV2Message::fromByteBuffer),
+
+ ARBITRARY_DATA(90, ArbitraryDataMessage::fromByteBuffer),
+ GET_ARBITRARY_DATA(91, GetArbitraryDataMessage::fromByteBuffer),
+
+ BLOCKS(100, null), // unsupported
+ GET_BLOCKS(101, null), // unsupported
+
+ ARBITRARY_DATA_FILE(110, ArbitraryDataFileMessage::fromByteBuffer),
+ GET_ARBITRARY_DATA_FILE(111, GetArbitraryDataFileMessage::fromByteBuffer),
+
+ ARBITRARY_DATA_FILE_LIST(120, ArbitraryDataFileListMessage::fromByteBuffer),
+ GET_ARBITRARY_DATA_FILE_LIST(121, GetArbitraryDataFileListMessage::fromByteBuffer),
+
+ ARBITRARY_SIGNATURES(130, ArbitrarySignaturesMessage::fromByteBuffer),
+
+ TRADE_PRESENCES(140, TradePresencesMessage::fromByteBuffer),
+ GET_TRADE_PRESENCES(141, GetTradePresencesMessage::fromByteBuffer),
+
+ ARBITRARY_METADATA(150, ArbitraryMetadataMessage::fromByteBuffer),
+ GET_ARBITRARY_METADATA(151, GetArbitraryMetadataMessage::fromByteBuffer);
+
+ public final int value;
+ public final MessageProducer fromByteBufferMethod;
+
+ private static final Map map = stream(MessageType.values())
+ .collect(toMap(messageType -> messageType.value, messageType -> messageType));
+
+ MessageType(int value, MessageProducer fromByteBufferMethod) {
+ this.value = value;
+ this.fromByteBufferMethod = fromByteBufferMethod;
+ }
+
+ public static MessageType valueOf(int value) {
+ return map.get(value);
+ }
+
+ /**
+ * Attempt to read a message from byte buffer.
+ *
+ * @param id message ID or -1
+ * @param byteBuffer ByteBuffer source for message
+ * @return null if no complete message can be read
+ * @throws MessageException if message could not be decoded or is invalid
+ * @throws BufferUnderflowException if not enough bytes in buffer to read message
+ */
+ public Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws MessageException {
+ if (this.fromByteBufferMethod == null)
+ throw new MessageException("Message type " + this.name() + " unsupported");
+
+ return this.fromByteBufferMethod.fromByteBuffer(id, byteBuffer);
+ }
+}
diff --git a/src/main/java/org/qortal/network/message/OnlineAccountsV2Message.java b/src/main/java/org/qortal/network/message/OnlineAccountsV2Message.java
index f0fce81e..6803e3bf 100644
--- a/src/main/java/org/qortal/network/message/OnlineAccountsV2Message.java
+++ b/src/main/java/org/qortal/network/message/OnlineAccountsV2Message.java
@@ -7,13 +7,11 @@ import org.qortal.transform.Transformer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.stream.Collectors;
/**
* For sending online accounts info to remote peer.
@@ -25,11 +23,52 @@ import java.util.stream.Collectors;
* Also V2 only builds online accounts message once!
*/
public class OnlineAccountsV2Message extends Message {
+
private List onlineAccounts;
- private byte[] cachedData;
public OnlineAccountsV2Message(List onlineAccounts) {
- this(-1, onlineAccounts);
+ super(MessageType.ONLINE_ACCOUNTS_V2);
+
+ // Shortcut in case we have no online accounts
+ if (onlineAccounts.isEmpty()) {
+ this.dataBytes = Ints.toByteArray(0);
+ this.checksumBytes = Message.generateChecksum(this.dataBytes);
+ return;
+ }
+
+ // How many of each timestamp
+ Map countByTimestamp = new HashMap<>();
+
+ for (OnlineAccountData onlineAccountData : onlineAccounts) {
+ Long timestamp = onlineAccountData.getTimestamp();
+ countByTimestamp.compute(timestamp, (k, v) -> v == null ? 1 : ++v);
+ }
+
+ // We should know exactly how many bytes to allocate now
+ int byteSize = countByTimestamp.size() * (Transformer.INT_LENGTH + Transformer.TIMESTAMP_LENGTH)
+ + onlineAccounts.size() * (Transformer.SIGNATURE_LENGTH + Transformer.PUBLIC_KEY_LENGTH);
+
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream(byteSize);
+
+ try {
+ for (long timestamp : countByTimestamp.keySet()) {
+ bytes.write(Ints.toByteArray(countByTimestamp.get(timestamp)));
+
+ bytes.write(Longs.toByteArray(timestamp));
+
+ for (OnlineAccountData onlineAccountData : onlineAccounts) {
+ if (onlineAccountData.getTimestamp() == timestamp) {
+ bytes.write(onlineAccountData.getSignature());
+ bytes.write(onlineAccountData.getPublicKey());
+ }
+ }
+ }
+ } catch (IOException e) {
+ throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
+ }
+
+ this.dataBytes = bytes.toByteArray();
+ this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private OnlineAccountsV2Message(int id, List onlineAccounts) {
@@ -42,7 +81,7 @@ public class OnlineAccountsV2Message extends Message {
return this.onlineAccounts;
}
- public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
+ public static Message fromByteBuffer(int id, ByteBuffer bytes) throws MessageException {
int accountCount = bytes.getInt();
List onlineAccounts = new ArrayList<>(accountCount);
@@ -71,54 +110,4 @@ public class OnlineAccountsV2Message extends Message {
return new OnlineAccountsV2Message(id, onlineAccounts);
}
- @Override
- protected synchronized byte[] toData() {
- if (this.cachedData != null)
- return this.cachedData;
-
- // Shortcut in case we have no online accounts
- if (this.onlineAccounts.isEmpty()) {
- this.cachedData = Ints.toByteArray(0);
- return this.cachedData;
- }
-
- // How many of each timestamp
- Map countByTimestamp = new HashMap<>();
-
- for (int i = 0; i < this.onlineAccounts.size(); ++i) {
- OnlineAccountData onlineAccountData = this.onlineAccounts.get(i);
- Long timestamp = onlineAccountData.getTimestamp();
- countByTimestamp.compute(timestamp, (k, v) -> v == null ? 1 : ++v);
- }
-
- // We should know exactly how many bytes to allocate now
- int byteSize = countByTimestamp.size() * (Transformer.INT_LENGTH + Transformer.TIMESTAMP_LENGTH)
- + this.onlineAccounts.size() * (Transformer.SIGNATURE_LENGTH + Transformer.PUBLIC_KEY_LENGTH);
-
- try {
- ByteArrayOutputStream bytes = new ByteArrayOutputStream(byteSize);
-
- for (long timestamp : countByTimestamp.keySet()) {
- bytes.write(Ints.toByteArray(countByTimestamp.get(timestamp)));
-
- bytes.write(Longs.toByteArray(timestamp));
-
- for (int i = 0; i < this.onlineAccounts.size(); ++i) {
- OnlineAccountData onlineAccountData = this.onlineAccounts.get(i);
-
- if (onlineAccountData.getTimestamp() == timestamp) {
- bytes.write(onlineAccountData.getSignature());
-
- bytes.write(onlineAccountData.getPublicKey());
- }
- }
- }
-
- this.cachedData = bytes.toByteArray();
- return this.cachedData;
- } catch (IOException e) {
- return null;
- }
- }
-
}
diff --git a/src/main/java/org/qortal/network/message/PeersV2Message.java b/src/main/java/org/qortal/network/message/PeersV2Message.java
index bfea87c7..e844246f 100644
--- a/src/main/java/org/qortal/network/message/PeersV2Message.java
+++ b/src/main/java/org/qortal/network/message/PeersV2Message.java
@@ -2,7 +2,6 @@ package org.qortal.network.message;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
@@ -19,7 +18,35 @@ public class PeersV2Message extends Message {
private List peerAddresses;
public PeersV2Message(List peerAddresses) {
- this(-1, peerAddresses);
+ super(MessageType.PEERS_V2);
+
+ List addresses = new ArrayList<>();
+
+ // First entry represents sending node but contains only port number with empty address.
+ addresses.add(("0.0.0.0:" + Settings.getInstance().getListenPort()).getBytes(StandardCharsets.UTF_8));
+
+ for (PeerAddress peerAddress : peerAddresses)
+ addresses.add(peerAddress.toString().getBytes(StandardCharsets.UTF_8));
+
+ // We can't send addresses that are longer than 255 bytes as length itself is encoded in one byte.
+ addresses.removeIf(addressString -> addressString.length > 255);
+
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+
+ try {
+ // Number of entries
+ bytes.write(Ints.toByteArray(addresses.size()));
+
+ for (byte[] address : addresses) {
+ bytes.write(address.length);
+ bytes.write(address);
+ }
+ } catch (IOException e) {
+ throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
+ }
+
+ this.dataBytes = bytes.toByteArray();
+ this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private PeersV2Message(int id, List peerAddresses) {
@@ -32,7 +59,7 @@ public class PeersV2Message extends Message {
return this.peerAddresses;
}
- public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws UnsupportedEncodingException {
+ public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws MessageException {
// Read entry count
int count = byteBuffer.getInt();
@@ -49,43 +76,11 @@ public class PeersV2Message extends Message {
PeerAddress peerAddress = PeerAddress.fromString(addressString);
peerAddresses.add(peerAddress);
} catch (IllegalArgumentException e) {
- // Not valid - ignore
+ throw new MessageException("Invalid peer address in received PEERS_V2 message");
}
}
return new PeersV2Message(id, peerAddresses);
}
- @Override
- protected byte[] toData() {
- try {
- ByteArrayOutputStream bytes = new ByteArrayOutputStream();
-
- List addresses = new ArrayList<>();
-
- // First entry represents sending node but contains only port number with empty address.
- addresses.add(("0.0.0.0:" + Settings.getInstance().getListenPort()).getBytes(StandardCharsets.UTF_8));
-
- for (PeerAddress peerAddress : this.peerAddresses)
- addresses.add(peerAddress.toString().getBytes(StandardCharsets.UTF_8));
-
- // We can't send addresses that are longer than 255 bytes as length itself is encoded in one byte.
- addresses.removeIf(addressString -> addressString.length > 255);
-
- // Serialize
-
- // Number of entries
- bytes.write(Ints.toByteArray(addresses.size()));
-
- for (byte[] address : addresses) {
- bytes.write(address.length);
- bytes.write(address);
- }
-
- return bytes.toByteArray();
- } catch (IOException e) {
- return null;
- }
- }
-
}
diff --git a/src/main/java/org/qortal/network/message/PingMessage.java b/src/main/java/org/qortal/network/message/PingMessage.java
index ddec0fd7..0b66d507 100644
--- a/src/main/java/org/qortal/network/message/PingMessage.java
+++ b/src/main/java/org/qortal/network/message/PingMessage.java
@@ -1,25 +1,21 @@
package org.qortal.network.message;
-import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
public class PingMessage extends Message {
public PingMessage() {
- this(-1);
+ super(MessageType.PING);
+
+ this.dataBytes = EMPTY_DATA_BYTES;
}
private PingMessage(int id) {
super(id, MessageType.PING);
}
- public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
+ public static Message fromByteBuffer(int id, ByteBuffer bytes) {
return new PingMessage(id);
}
- @Override
- protected byte[] toData() {
- return new byte[0];
- }
-
}
diff --git a/src/main/java/org/qortal/network/message/PongMessage.java b/src/main/java/org/qortal/network/message/PongMessage.java
new file mode 100644
index 00000000..4e73c07c
--- /dev/null
+++ b/src/main/java/org/qortal/network/message/PongMessage.java
@@ -0,0 +1,21 @@
+package org.qortal.network.message;
+
+import java.nio.ByteBuffer;
+
+public class PongMessage extends Message {
+
+ public PongMessage() {
+ super(MessageType.PONG);
+
+ this.dataBytes = EMPTY_DATA_BYTES;
+ }
+
+ private PongMessage(int id) {
+ super(id, MessageType.PONG);
+ }
+
+ public static Message fromByteBuffer(int id, ByteBuffer bytes) {
+ return new PongMessage(id);
+ }
+
+}
diff --git a/src/main/java/org/qortal/network/message/ResponseMessage.java b/src/main/java/org/qortal/network/message/ResponseMessage.java
index 6fed6d6a..292fe697 100644
--- a/src/main/java/org/qortal/network/message/ResponseMessage.java
+++ b/src/main/java/org/qortal/network/message/ResponseMessage.java
@@ -10,8 +10,25 @@ public class ResponseMessage extends Message {
public static final int DATA_LENGTH = 32;
- private final int nonce;
- private final byte[] data;
+ private int nonce;
+ private byte[] data;
+
+ public ResponseMessage(int nonce, byte[] data) {
+ super(MessageType.RESPONSE);
+
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream(4 + DATA_LENGTH);
+
+ try {
+ bytes.write(Ints.toByteArray(nonce));
+
+ bytes.write(data);
+ } catch (IOException e) {
+ throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
+ }
+
+ this.dataBytes = bytes.toByteArray();
+ this.checksumBytes = Message.generateChecksum(this.dataBytes);
+ }
private ResponseMessage(int id, int nonce, byte[] data) {
super(id, MessageType.RESPONSE);
@@ -20,10 +37,6 @@ public class ResponseMessage extends Message {
this.data = data;
}
- public ResponseMessage(int nonce, byte[] data) {
- this(-1, nonce, data);
- }
-
public int getNonce() {
return this.nonce;
}
@@ -41,15 +54,4 @@ public class ResponseMessage extends Message {
return new ResponseMessage(id, nonce, data);
}
- @Override
- protected byte[] toData() throws IOException {
- ByteArrayOutputStream bytes = new ByteArrayOutputStream(4 + DATA_LENGTH);
-
- bytes.write(Ints.toByteArray(this.nonce));
-
- bytes.write(data);
-
- return bytes.toByteArray();
- }
-
}
diff --git a/src/main/java/org/qortal/network/message/SignaturesMessage.java b/src/main/java/org/qortal/network/message/SignaturesMessage.java
index 008f4c1a..c0b44fcd 100644
--- a/src/main/java/org/qortal/network/message/SignaturesMessage.java
+++ b/src/main/java/org/qortal/network/message/SignaturesMessage.java
@@ -2,7 +2,7 @@ package org.qortal.network.message;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.io.UnsupportedEncodingException;
+import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
@@ -13,12 +13,24 @@ import com.google.common.primitives.Ints;
public class SignaturesMessage extends Message {
- private static final int BLOCK_SIGNATURE_LENGTH = BlockTransformer.BLOCK_SIGNATURE_LENGTH;
-
private List signatures;
public SignaturesMessage(List signatures) {
- this(-1, signatures);
+ super(MessageType.SIGNATURES);
+
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+
+ try {
+ bytes.write(Ints.toByteArray(signatures.size()));
+
+ for (byte[] signature : signatures)
+ bytes.write(signature);
+ } catch (IOException e) {
+ throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
+ }
+
+ this.dataBytes = bytes.toByteArray();
+ this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private SignaturesMessage(int id, List signatures) {
@@ -31,15 +43,15 @@ public class SignaturesMessage extends Message {
return this.signatures;
}
- public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
+ public static Message fromByteBuffer(int id, ByteBuffer bytes) {
int count = bytes.getInt();
- if (bytes.remaining() != count * BLOCK_SIGNATURE_LENGTH)
- return null;
+ if (bytes.remaining() < count * BlockTransformer.BLOCK_SIGNATURE_LENGTH)
+ throw new BufferUnderflowException();
List signatures = new ArrayList<>();
for (int i = 0; i < count; ++i) {
- byte[] signature = new byte[BLOCK_SIGNATURE_LENGTH];
+ byte[] signature = new byte[BlockTransformer.BLOCK_SIGNATURE_LENGTH];
bytes.get(signature);
signatures.add(signature);
}
@@ -47,20 +59,4 @@ public class SignaturesMessage extends Message {
return new SignaturesMessage(id, signatures);
}
- @Override
- protected byte[] toData() {
- try {
- ByteArrayOutputStream bytes = new ByteArrayOutputStream();
-
- bytes.write(Ints.toByteArray(this.signatures.size()));
-
- for (byte[] signature : this.signatures)
- bytes.write(signature);
-
- return bytes.toByteArray();
- } catch (IOException e) {
- return null;
- }
- }
-
}
diff --git a/src/main/java/org/qortal/network/message/TradePresencesMessage.java b/src/main/java/org/qortal/network/message/TradePresencesMessage.java
index 9d846722..8d7da156 100644
--- a/src/main/java/org/qortal/network/message/TradePresencesMessage.java
+++ b/src/main/java/org/qortal/network/message/TradePresencesMessage.java
@@ -8,7 +8,6 @@ import org.qortal.utils.Base58;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
@@ -21,11 +20,55 @@ import java.util.Map;
* Groups of: number of entries, timestamp, then pubkey + sig + AT address for each entry.
*/
public class TradePresencesMessage extends Message {
+
private List tradePresences;
- private byte[] cachedData;
public TradePresencesMessage(List tradePresences) {
- this(-1, tradePresences);
+ super(MessageType.TRADE_PRESENCES);
+
+ // Shortcut in case we have no trade presences
+ if (tradePresences.isEmpty()) {
+ this.dataBytes = Ints.toByteArray(0);
+ this.checksumBytes = Message.generateChecksum(this.dataBytes);
+ return;
+ }
+
+ // How many of each timestamp
+ Map countByTimestamp = new HashMap<>();
+
+ for (TradePresenceData tradePresenceData : tradePresences) {
+ Long timestamp = tradePresenceData.getTimestamp();
+ countByTimestamp.compute(timestamp, (k, v) -> v == null ? 1 : ++v);
+ }
+
+ // We should know exactly how many bytes to allocate now
+ int byteSize = countByTimestamp.size() * (Transformer.INT_LENGTH + Transformer.TIMESTAMP_LENGTH)
+ + tradePresences.size() * (Transformer.PUBLIC_KEY_LENGTH + Transformer.SIGNATURE_LENGTH + Transformer.ADDRESS_LENGTH);
+
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream(byteSize);
+
+ try {
+ for (long timestamp : countByTimestamp.keySet()) {
+ bytes.write(Ints.toByteArray(countByTimestamp.get(timestamp)));
+
+ bytes.write(Longs.toByteArray(timestamp));
+
+ for (TradePresenceData tradePresenceData : tradePresences) {
+ if (tradePresenceData.getTimestamp() == timestamp) {
+ bytes.write(tradePresenceData.getPublicKey());
+
+ bytes.write(tradePresenceData.getSignature());
+
+ bytes.write(Base58.decode(tradePresenceData.getAtAddress()));
+ }
+ }
+ }
+ } catch (IOException e) {
+ throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
+ }
+
+ this.dataBytes = bytes.toByteArray();
+ this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private TradePresencesMessage(int id, List tradePresences) {
@@ -38,7 +81,7 @@ public class TradePresencesMessage extends Message {
return this.tradePresences;
}
- public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
+ public static Message fromByteBuffer(int id, ByteBuffer bytes) {
int groupedEntriesCount = bytes.getInt();
List tradePresences = new ArrayList<>(groupedEntriesCount);
@@ -71,53 +114,4 @@ public class TradePresencesMessage extends Message {
return new TradePresencesMessage(id, tradePresences);
}
- @Override
- protected synchronized byte[] toData() {
- if (this.cachedData != null)
- return this.cachedData;
-
- // Shortcut in case we have no trade presences
- if (this.tradePresences.isEmpty()) {
- this.cachedData = Ints.toByteArray(0);
- return this.cachedData;
- }
-
- // How many of each timestamp
- Map countByTimestamp = new HashMap<>();
-
- for (TradePresenceData tradePresenceData : this.tradePresences) {
- Long timestamp = tradePresenceData.getTimestamp();
- countByTimestamp.compute(timestamp, (k, v) -> v == null ? 1 : ++v);
- }
-
- // We should know exactly how many bytes to allocate now
- int byteSize = countByTimestamp.size() * (Transformer.INT_LENGTH + Transformer.TIMESTAMP_LENGTH)
- + this.tradePresences.size() * (Transformer.PUBLIC_KEY_LENGTH + Transformer.SIGNATURE_LENGTH + Transformer.ADDRESS_LENGTH);
-
- try {
- ByteArrayOutputStream bytes = new ByteArrayOutputStream(byteSize);
-
- for (long timestamp : countByTimestamp.keySet()) {
- bytes.write(Ints.toByteArray(countByTimestamp.get(timestamp)));
-
- bytes.write(Longs.toByteArray(timestamp));
-
- for (TradePresenceData tradePresenceData : this.tradePresences) {
- if (tradePresenceData.getTimestamp() == timestamp) {
- bytes.write(tradePresenceData.getPublicKey());
-
- bytes.write(tradePresenceData.getSignature());
-
- bytes.write(Base58.decode(tradePresenceData.getAtAddress()));
- }
- }
- }
-
- this.cachedData = bytes.toByteArray();
- return this.cachedData;
- } catch (IOException e) {
- return null;
- }
- }
-
}
diff --git a/src/main/java/org/qortal/network/message/TransactionMessage.java b/src/main/java/org/qortal/network/message/TransactionMessage.java
index 92cce086..51db6cf9 100644
--- a/src/main/java/org/qortal/network/message/TransactionMessage.java
+++ b/src/main/java/org/qortal/network/message/TransactionMessage.java
@@ -1,6 +1,5 @@
package org.qortal.network.message;
-import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import org.qortal.data.transaction.TransactionData;
@@ -11,8 +10,11 @@ public class TransactionMessage extends Message {
private TransactionData transactionData;
- public TransactionMessage(TransactionData transactionData) {
- this(-1, transactionData);
+ public TransactionMessage(TransactionData transactionData) throws TransformationException {
+ super(MessageType.TRANSACTION);
+
+ this.dataBytes = TransactionTransformer.toBytes(transactionData);
+ this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private TransactionMessage(int id, TransactionData transactionData) {
@@ -25,26 +27,16 @@ public class TransactionMessage extends Message {
return this.transactionData;
}
- public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws UnsupportedEncodingException {
- try {
- TransactionData transactionData = TransactionTransformer.fromByteBuffer(byteBuffer);
-
- return new TransactionMessage(id, transactionData);
- } catch (TransformationException e) {
- return null;
- }
- }
-
- @Override
- protected byte[] toData() {
- if (this.transactionData == null)
- return null;
+ public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws MessageException {
+ TransactionData transactionData;
try {
- return TransactionTransformer.toBytes(this.transactionData);
+ transactionData = TransactionTransformer.fromByteBuffer(byteBuffer);
} catch (TransformationException e) {
- return null;
+ throw new MessageException(e.getMessage(), e);
}
+
+ return new TransactionMessage(id, transactionData);
}
}
diff --git a/src/main/java/org/qortal/network/message/TransactionSignaturesMessage.java b/src/main/java/org/qortal/network/message/TransactionSignaturesMessage.java
index 082a7187..395d3f00 100644
--- a/src/main/java/org/qortal/network/message/TransactionSignaturesMessage.java
+++ b/src/main/java/org/qortal/network/message/TransactionSignaturesMessage.java
@@ -2,7 +2,7 @@ package org.qortal.network.message;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.io.UnsupportedEncodingException;
+import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
@@ -13,12 +13,24 @@ import com.google.common.primitives.Ints;
public class TransactionSignaturesMessage extends Message {
- private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH;
-
private List signatures;
public TransactionSignaturesMessage(List signatures) {
- this(-1, signatures);
+ super(MessageType.TRANSACTION_SIGNATURES);
+
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+
+ try {
+ bytes.write(Ints.toByteArray(signatures.size()));
+
+ for (byte[] signature : signatures)
+ bytes.write(signature);
+ } catch (IOException e) {
+ throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
+ }
+
+ this.dataBytes = bytes.toByteArray();
+ this.checksumBytes = Message.generateChecksum(this.dataBytes);
}
private TransactionSignaturesMessage(int id, List signatures) {
@@ -31,15 +43,15 @@ public class TransactionSignaturesMessage extends Message {
return this.signatures;
}
- public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException {
+ public static Message fromByteBuffer(int id, ByteBuffer bytes) {
int count = bytes.getInt();
- if (bytes.remaining() != count * SIGNATURE_LENGTH)
- return null;
+ if (bytes.remaining() < count * Transformer.SIGNATURE_LENGTH)
+ throw new BufferUnderflowException();
List signatures = new ArrayList<>();
for (int i = 0; i < count; ++i) {
- byte[] signature = new byte[SIGNATURE_LENGTH];
+ byte[] signature = new byte[Transformer.SIGNATURE_LENGTH];
bytes.get(signature);
signatures.add(signature);
}
@@ -47,20 +59,4 @@ public class TransactionSignaturesMessage extends Message {
return new TransactionSignaturesMessage(id, signatures);
}
- @Override
- protected byte[] toData() {
- try {
- ByteArrayOutputStream bytes = new ByteArrayOutputStream();
-
- bytes.write(Ints.toByteArray(this.signatures.size()));
-
- for (byte[] signature : this.signatures)
- bytes.write(signature);
-
- return bytes.toByteArray();
- } catch (IOException e) {
- return null;
- }
- }
-
}
diff --git a/src/main/java/org/qortal/network/task/BroadcastTask.java b/src/main/java/org/qortal/network/task/BroadcastTask.java
new file mode 100644
index 00000000..5714ebf6
--- /dev/null
+++ b/src/main/java/org/qortal/network/task/BroadcastTask.java
@@ -0,0 +1,22 @@
+package org.qortal.network.task;
+
+import org.qortal.controller.Controller;
+import org.qortal.network.Network;
+import org.qortal.network.Peer;
+import org.qortal.network.message.Message;
+import org.qortal.utils.ExecuteProduceConsume.Task;
+
+public class BroadcastTask implements Task {
+ public BroadcastTask() {
+ }
+
+ @Override
+ public String getName() {
+ return "BroadcastTask";
+ }
+
+ @Override
+ public void perform() throws InterruptedException {
+ Controller.getInstance().doNetworkBroadcast();
+ }
+}
diff --git a/src/main/java/org/qortal/network/task/ChannelAcceptTask.java b/src/main/java/org/qortal/network/task/ChannelAcceptTask.java
new file mode 100644
index 00000000..3e2a3033
--- /dev/null
+++ b/src/main/java/org/qortal/network/task/ChannelAcceptTask.java
@@ -0,0 +1,97 @@
+package org.qortal.network.task;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.qortal.network.Network;
+import org.qortal.network.Peer;
+import org.qortal.network.PeerAddress;
+import org.qortal.settings.Settings;
+import org.qortal.utils.ExecuteProduceConsume.Task;
+import org.qortal.utils.NTP;
+
+import java.io.IOException;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.ServerSocketChannel;
+import java.nio.channels.SocketChannel;
+import java.util.List;
+
+public class ChannelAcceptTask implements Task {
+ private static final Logger LOGGER = LogManager.getLogger(ChannelAcceptTask.class);
+
+ private final ServerSocketChannel serverSocketChannel;
+
+ public ChannelAcceptTask(ServerSocketChannel serverSocketChannel) {
+ this.serverSocketChannel = serverSocketChannel;
+ }
+
+ @Override
+ public String getName() {
+ return "ChannelAcceptTask";
+ }
+
+ @Override
+ public void perform() throws InterruptedException {
+ Network network = Network.getInstance();
+ SocketChannel socketChannel;
+
+ try {
+ if (network.getImmutableConnectedPeers().size() >= network.getMaxPeers()) {
+ // We have enough peers
+ LOGGER.debug("Ignoring pending incoming connections because the server is full");
+ return;
+ }
+
+ socketChannel = serverSocketChannel.accept();
+
+ network.setInterestOps(serverSocketChannel, SelectionKey.OP_ACCEPT);
+ } catch (IOException e) {
+ return;
+ }
+
+ // No connection actually accepted?
+ if (socketChannel == null) {
+ return;
+ }
+
+ PeerAddress address = PeerAddress.fromSocket(socketChannel.socket());
+ List fixedNetwork = Settings.getInstance().getFixedNetwork();
+ if (fixedNetwork != null && !fixedNetwork.isEmpty() && network.ipNotInFixedList(address, fixedNetwork)) {
+ try {
+ LOGGER.debug("Connection discarded from peer {} as not in the fixed network list", address);
+ socketChannel.close();
+ } catch (IOException e) {
+ // IGNORE
+ }
+ return;
+ }
+
+ final Long now = NTP.getTime();
+ Peer newPeer;
+
+ try {
+ if (now == null) {
+ LOGGER.debug("Connection discarded from peer {} due to lack of NTP sync", address);
+ socketChannel.close();
+ return;
+ }
+
+ LOGGER.debug("Connection accepted from peer {}", address);
+
+ newPeer = new Peer(socketChannel);
+ network.addConnectedPeer(newPeer);
+
+ } catch (IOException e) {
+ if (socketChannel.isOpen()) {
+ try {
+ LOGGER.debug("Connection failed from peer {} while connecting/closing", address);
+ socketChannel.close();
+ } catch (IOException ce) {
+ // Couldn't close?
+ }
+ }
+ return;
+ }
+
+ network.onPeerReady(newPeer);
+ }
+}
diff --git a/src/main/java/org/qortal/network/task/ChannelReadTask.java b/src/main/java/org/qortal/network/task/ChannelReadTask.java
new file mode 100644
index 00000000..edd4e8c0
--- /dev/null
+++ b/src/main/java/org/qortal/network/task/ChannelReadTask.java
@@ -0,0 +1,49 @@
+package org.qortal.network.task;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import org.qortal.network.Network;
+import org.qortal.network.Peer;
+import org.qortal.utils.ExecuteProduceConsume.Task;
+
+import java.io.IOException;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.SocketChannel;
+
+public class ChannelReadTask implements Task {
+ private static final Logger LOGGER = LogManager.getLogger(ChannelReadTask.class);
+
+ private final SocketChannel socketChannel;
+ private final Peer peer;
+ private final String name;
+
+ public ChannelReadTask(SocketChannel socketChannel, Peer peer) {
+ this.socketChannel = socketChannel;
+ this.peer = peer;
+ this.name = "ChannelReadTask::" + peer;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public void perform() throws InterruptedException {
+ try {
+ peer.readChannel();
+
+ Network.getInstance().setInterestOps(socketChannel, SelectionKey.OP_READ);
+ } catch (IOException e) {
+ if (e.getMessage() != null && e.getMessage().toLowerCase().contains("connection reset")) {
+ peer.disconnect("Connection reset");
+ return;
+ }
+
+ LOGGER.trace("[{}] Network thread {} encountered I/O error: {}", peer.getPeerConnectionId(),
+ Thread.currentThread().getId(), e.getMessage(), e);
+ peer.disconnect("I/O error");
+ }
+ }
+}
diff --git a/src/main/java/org/qortal/network/task/ChannelWriteTask.java b/src/main/java/org/qortal/network/task/ChannelWriteTask.java
new file mode 100644
index 00000000..59bc557e
--- /dev/null
+++ b/src/main/java/org/qortal/network/task/ChannelWriteTask.java
@@ -0,0 +1,52 @@
+package org.qortal.network.task;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.qortal.network.Network;
+import org.qortal.network.Peer;
+import org.qortal.utils.ExecuteProduceConsume.Task;
+
+import java.io.IOException;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.SocketChannel;
+
+public class ChannelWriteTask implements Task {
+ private static final Logger LOGGER = LogManager.getLogger(ChannelWriteTask.class);
+
+ private final SocketChannel socketChannel;
+ private final Peer peer;
+ private final String name;
+
+ public ChannelWriteTask(SocketChannel socketChannel, Peer peer) {
+ this.socketChannel = socketChannel;
+ this.peer = peer;
+ this.name = "ChannelWriteTask::" + peer;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public void perform() throws InterruptedException {
+ try {
+ boolean isSocketClogged = peer.writeChannel();
+
+ // Tell Network that we've finished
+ Network.getInstance().notifyChannelNotWriting(socketChannel);
+
+ if (isSocketClogged)
+ Network.getInstance().setInterestOps(this.socketChannel, SelectionKey.OP_WRITE);
+ } catch (IOException e) {
+ if (e.getMessage() != null && e.getMessage().toLowerCase().contains("connection reset")) {
+ peer.disconnect("Connection reset");
+ return;
+ }
+
+ LOGGER.trace("[{}] Network thread {} encountered I/O error: {}", peer.getPeerConnectionId(),
+ Thread.currentThread().getId(), e.getMessage(), e);
+ peer.disconnect("I/O error");
+ }
+ }
+}
diff --git a/src/main/java/org/qortal/network/task/MessageTask.java b/src/main/java/org/qortal/network/task/MessageTask.java
new file mode 100644
index 00000000..c1907b62
--- /dev/null
+++ b/src/main/java/org/qortal/network/task/MessageTask.java
@@ -0,0 +1,28 @@
+package org.qortal.network.task;
+
+import org.qortal.network.Network;
+import org.qortal.network.Peer;
+import org.qortal.network.message.Message;
+import org.qortal.utils.ExecuteProduceConsume.Task;
+
+public class MessageTask implements Task {
+ private final Peer peer;
+ private final Message nextMessage;
+ private final String name;
+
+ public MessageTask(Peer peer, Message nextMessage) {
+ this.peer = peer;
+ this.nextMessage = nextMessage;
+ this.name = "MessageTask::" + peer + "::" + nextMessage.getType();
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public void perform() throws InterruptedException {
+ Network.getInstance().onMessage(peer, nextMessage);
+ }
+}
diff --git a/src/main/java/org/qortal/network/task/PeerConnectTask.java b/src/main/java/org/qortal/network/task/PeerConnectTask.java
new file mode 100644
index 00000000..759cabce
--- /dev/null
+++ b/src/main/java/org/qortal/network/task/PeerConnectTask.java
@@ -0,0 +1,33 @@
+package org.qortal.network.task;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.qortal.network.Network;
+import org.qortal.network.Peer;
+import org.qortal.network.message.Message;
+import org.qortal.network.message.MessageType;
+import org.qortal.network.message.PingMessage;
+import org.qortal.utils.ExecuteProduceConsume.Task;
+import org.qortal.utils.NTP;
+
+public class PeerConnectTask implements Task {
+ private static final Logger LOGGER = LogManager.getLogger(PeerConnectTask.class);
+
+ private final Peer peer;
+ private final String name;
+
+ public PeerConnectTask(Peer peer) {
+ this.peer = peer;
+ this.name = "PeerConnectTask::" + peer;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public void perform() throws InterruptedException {
+ Network.getInstance().connectPeer(peer);
+ }
+}
diff --git a/src/main/java/org/qortal/network/task/PingTask.java b/src/main/java/org/qortal/network/task/PingTask.java
new file mode 100644
index 00000000..f47ecd32
--- /dev/null
+++ b/src/main/java/org/qortal/network/task/PingTask.java
@@ -0,0 +1,44 @@
+package org.qortal.network.task;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.qortal.network.Peer;
+import org.qortal.network.message.Message;
+import org.qortal.network.message.MessageType;
+import org.qortal.network.message.PingMessage;
+import org.qortal.utils.ExecuteProduceConsume.Task;
+import org.qortal.utils.NTP;
+
+public class PingTask implements Task {
+ private static final Logger LOGGER = LogManager.getLogger(PingTask.class);
+
+ private final Peer peer;
+ private final Long now;
+ private final String name;
+
+ public PingTask(Peer peer, Long now) {
+ this.peer = peer;
+ this.now = now;
+ this.name = "PingTask::" + peer;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public void perform() throws InterruptedException {
+ PingMessage pingMessage = new PingMessage();
+ Message message = peer.getResponse(pingMessage);
+
+ if (message == null || message.getType() != MessageType.PING) {
+ LOGGER.debug("[{}] Didn't receive reply from {} for PING ID {}",
+ peer.getPeerConnectionId(), peer, pingMessage.getId());
+ peer.disconnect("no ping received");
+ return;
+ }
+
+ peer.setLastPing(NTP.getTime() - now);
+ }
+}
diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java
index d9791475..2e3d0859 100644
--- a/src/main/java/org/qortal/settings/Settings.java
+++ b/src/main/java/org/qortal/settings/Settings.java
@@ -26,6 +26,8 @@ import org.qortal.controller.arbitrary.ArbitraryDataStorageManager.*;
import org.qortal.crosschain.Bitcoin.BitcoinNet;
import org.qortal.crosschain.Litecoin.LitecoinNet;
import org.qortal.crosschain.Dogecoin.DogecoinNet;
+import org.qortal.crosschain.Digibyte.DigibyteNet;
+import org.qortal.crosschain.Ravencoin.RavencoinNet;
import org.qortal.utils.EnumUtils;
// All properties to be converted to JSON via JAXB
@@ -222,6 +224,8 @@ public class Settings {
private BitcoinNet bitcoinNet = BitcoinNet.MAIN;
private LitecoinNet litecoinNet = LitecoinNet.MAIN;
private DogecoinNet dogecoinNet = DogecoinNet.MAIN;
+ private DigibyteNet digibyteNet = DigibyteNet.MAIN;
+ private RavencoinNet ravencoinNet = RavencoinNet.MAIN;
// Also crosschain-related:
/** Whether to show SysTray pop-up notifications when trade-bot entries change state */
private boolean tradebotSystrayEnabled = false;
@@ -685,6 +689,14 @@ public class Settings {
return this.dogecoinNet;
}
+ public DigibyteNet getDigibyteNet() {
+ return this.digibyteNet;
+ }
+
+ public RavencoinNet getRavencoinNet() {
+ return this.ravencoinNet;
+ }
+
public boolean isTradebotSystrayEnabled() {
return this.tradebotSystrayEnabled;
}
diff --git a/src/main/java/org/qortal/transform/transaction/ArbitraryTransactionTransformer.java b/src/main/java/org/qortal/transform/transaction/ArbitraryTransactionTransformer.java
index c3190f03..b1554e8d 100644
--- a/src/main/java/org/qortal/transform/transaction/ArbitraryTransactionTransformer.java
+++ b/src/main/java/org/qortal/transform/transaction/ArbitraryTransactionTransformer.java
@@ -39,12 +39,12 @@ public class ArbitraryTransactionTransformer extends TransactionTransformer {
private static final int IDENTIFIER_SIZE_LENGTH = INT_LENGTH;
private static final int COMPRESSION_LENGTH = INT_LENGTH;
private static final int METHOD_LENGTH = INT_LENGTH;
- private static final int SECRET_LENGTH = INT_LENGTH; // TODO: wtf?
+ private static final int SECRET_SIZE_LENGTH = INT_LENGTH;
private static final int EXTRAS_LENGTH = SERVICE_LENGTH + DATA_TYPE_LENGTH + DATA_SIZE_LENGTH;
private static final int EXTRAS_V5_LENGTH = NONCE_LENGTH + NAME_SIZE_LENGTH + IDENTIFIER_SIZE_LENGTH +
- METHOD_LENGTH + SECRET_LENGTH + COMPRESSION_LENGTH + RAW_DATA_SIZE_LENGTH + METADATA_HASH_SIZE_LENGTH;
+ METHOD_LENGTH + SECRET_SIZE_LENGTH + COMPRESSION_LENGTH + RAW_DATA_SIZE_LENGTH + METADATA_HASH_SIZE_LENGTH;
protected static final TransactionLayout layout;
diff --git a/src/main/java/org/qortal/utils/ExecuteProduceConsume.java b/src/main/java/org/qortal/utils/ExecuteProduceConsume.java
index 57caab9c..223d0e93 100644
--- a/src/main/java/org/qortal/utils/ExecuteProduceConsume.java
+++ b/src/main/java/org/qortal/utils/ExecuteProduceConsume.java
@@ -28,7 +28,6 @@ public abstract class ExecuteProduceConsume implements Runnable {
private final String className;
private final Logger logger;
- private final boolean isLoggerTraceEnabled;
protected ExecutorService executor;
@@ -43,12 +42,12 @@ public abstract class ExecuteProduceConsume implements Runnable {
private volatile int tasksConsumed = 0;
private volatile int spawnFailures = 0;
+ /** Whether a new thread has already been spawned and is waiting to start. Used to prevent spawning multiple new threads. */
private volatile boolean hasThreadPending = false;
public ExecuteProduceConsume(ExecutorService executor) {
this.className = this.getClass().getSimpleName();
this.logger = LogManager.getLogger(this.getClass());
- this.isLoggerTraceEnabled = this.logger.isTraceEnabled();
this.executor = executor;
}
@@ -98,15 +97,14 @@ public abstract class ExecuteProduceConsume implements Runnable {
*/
protected abstract Task produceTask(boolean canBlock) throws InterruptedException;
- @FunctionalInterface
public interface Task {
- public abstract void perform() throws InterruptedException;
+ String getName();
+ void perform() throws InterruptedException;
}
@Override
public void run() {
- if (this.isLoggerTraceEnabled)
- Thread.currentThread().setName(this.className + "-" + Thread.currentThread().getId());
+ Thread.currentThread().setName(this.className + "-" + Thread.currentThread().getId());
boolean wasThreadPending;
synchronized (this) {
@@ -114,25 +112,19 @@ public abstract class ExecuteProduceConsume implements Runnable {
if (this.activeThreadCount > this.greatestActiveThreadCount)
this.greatestActiveThreadCount = this.activeThreadCount;
- if (this.isLoggerTraceEnabled) {
- this.logger.trace(() -> String.format("[%d] started, hasThreadPending was: %b, activeThreadCount now: %d",
- Thread.currentThread().getId(), this.hasThreadPending, this.activeThreadCount));
- }
+ this.logger.trace(() -> String.format("[%d] started, hasThreadPending was: %b, activeThreadCount now: %d",
+ Thread.currentThread().getId(), this.hasThreadPending, this.activeThreadCount));
// Defer clearing hasThreadPending to prevent unnecessary threads waiting to produce...
wasThreadPending = this.hasThreadPending;
}
try {
- // It's possible this might need to become a class instance private volatile
- boolean canBlock = false;
-
while (!Thread.currentThread().isInterrupted()) {
Task task = null;
+ String taskType;
- if (this.isLoggerTraceEnabled) {
- this.logger.trace(() -> String.format("[%d] waiting to produce...", Thread.currentThread().getId()));
- }
+ this.logger.trace(() -> String.format("[%d] waiting to produce...", Thread.currentThread().getId()));
synchronized (this) {
if (wasThreadPending) {
@@ -141,13 +133,13 @@ public abstract class ExecuteProduceConsume implements Runnable {
wasThreadPending = false;
}
- final boolean lambdaCanIdle = canBlock;
- if (this.isLoggerTraceEnabled) {
- this.logger.trace(() -> String.format("[%d] producing, activeThreadCount: %d, consumerCount: %d, canBlock is %b...",
- Thread.currentThread().getId(), this.activeThreadCount, this.consumerCount, lambdaCanIdle));
- }
+ // If we're the only non-consuming thread - producer can afford to block this round
+ boolean canBlock = this.activeThreadCount - this.consumerCount <= 1;
- final long beforeProduce = isLoggerTraceEnabled ? System.currentTimeMillis() : 0;
+ this.logger.trace(() -> String.format("[%d] producing... [activeThreadCount: %d, consumerCount: %d, canBlock: %b]",
+ Thread.currentThread().getId(), this.activeThreadCount, this.consumerCount, canBlock));
+
+ final long beforeProduce = this.logger.isDebugEnabled() ? System.currentTimeMillis() : 0;
try {
task = produceTask(canBlock);
@@ -158,31 +150,36 @@ public abstract class ExecuteProduceConsume implements Runnable {
this.logger.warn(() -> String.format("[%d] exception while trying to produce task", Thread.currentThread().getId()), e);
}
- if (this.isLoggerTraceEnabled) {
- this.logger.trace(() -> String.format("[%d] producing took %dms", Thread.currentThread().getId(), System.currentTimeMillis() - beforeProduce));
+ if (this.logger.isDebugEnabled()) {
+ final long productionPeriod = System.currentTimeMillis() - beforeProduce;
+ taskType = task == null ? "no task" : task.getName();
+
+ this.logger.debug(() -> String.format("[%d] produced [%s] in %dms [canBlock: %b]",
+ Thread.currentThread().getId(),
+ taskType,
+ productionPeriod,
+ canBlock
+ ));
+ } else {
+ taskType = null;
}
}
if (task == null)
synchronized (this) {
- if (this.isLoggerTraceEnabled) {
- this.logger.trace(() -> String.format("[%d] no task, activeThreadCount: %d, consumerCount: %d",
- Thread.currentThread().getId(), this.activeThreadCount, this.consumerCount));
- }
+ this.logger.trace(() -> String.format("[%d] no task, activeThreadCount: %d, consumerCount: %d",
+ Thread.currentThread().getId(), this.activeThreadCount, this.consumerCount));
- if (this.activeThreadCount > this.consumerCount + 1) {
+ // If we have an excess of non-consuming threads then we can exit
+ if (this.activeThreadCount - this.consumerCount > 1) {
--this.activeThreadCount;
- if (this.isLoggerTraceEnabled) {
- this.logger.trace(() -> String.format("[%d] ending, activeThreadCount now: %d",
- Thread.currentThread().getId(), this.activeThreadCount));
- }
+
+ this.logger.trace(() -> String.format("[%d] ending, activeThreadCount now: %d",
+ Thread.currentThread().getId(), this.activeThreadCount));
return;
}
- // We're the last surviving thread - producer can afford to block next round
- canBlock = true;
-
continue;
}
@@ -192,16 +189,13 @@ public abstract class ExecuteProduceConsume implements Runnable {
++this.tasksProduced;
++this.consumerCount;
- if (this.isLoggerTraceEnabled) {
- this.logger.trace(() -> String.format("[%d] hasThreadPending: %b, activeThreadCount: %d, consumerCount now: %d",
- Thread.currentThread().getId(), this.hasThreadPending, this.activeThreadCount, this.consumerCount));
- }
+ this.logger.trace(() -> String.format("[%d] hasThreadPending: %b, activeThreadCount: %d, consumerCount now: %d",
+ Thread.currentThread().getId(), this.hasThreadPending, this.activeThreadCount, this.consumerCount));
// If we have no thread pending and no excess of threads then we should spawn a fresh thread
- if (!this.hasThreadPending && this.activeThreadCount <= this.consumerCount + 1) {
- if (this.isLoggerTraceEnabled) {
- this.logger.trace(() -> String.format("[%d] spawning another thread", Thread.currentThread().getId()));
- }
+ if (!this.hasThreadPending && this.activeThreadCount == this.consumerCount) {
+ this.logger.trace(() -> String.format("[%d] spawning another thread", Thread.currentThread().getId()));
+
this.hasThreadPending = true;
try {
@@ -209,21 +203,19 @@ public abstract class ExecuteProduceConsume implements Runnable {
} catch (RejectedExecutionException e) {
++this.spawnFailures;
this.hasThreadPending = false;
- if (this.isLoggerTraceEnabled) {
- this.logger.trace(() -> String.format("[%d] failed to spawn another thread", Thread.currentThread().getId()));
- }
+
+ this.logger.trace(() -> String.format("[%d] failed to spawn another thread", Thread.currentThread().getId()));
+
this.onSpawnFailure();
}
} else {
- if (this.isLoggerTraceEnabled) {
- this.logger.trace(() -> String.format("[%d] NOT spawning another thread", Thread.currentThread().getId()));
- }
+ this.logger.trace(() -> String.format("[%d] NOT spawning another thread", Thread.currentThread().getId()));
}
}
- if (this.isLoggerTraceEnabled) {
- this.logger.trace(() -> String.format("[%d] performing task...", Thread.currentThread().getId()));
- }
+ this.logger.trace(() -> String.format("[%d] consuming [%s] task...", Thread.currentThread().getId(), taskType));
+
+ final long beforePerform = this.logger.isDebugEnabled() ? System.currentTimeMillis() : 0;
try {
task.perform(); // This can block for a while
@@ -231,29 +223,25 @@ public abstract class ExecuteProduceConsume implements Runnable {
// We're in shutdown situation so exit
Thread.currentThread().interrupt();
} catch (Exception e) {
- this.logger.warn(() -> String.format("[%d] exception while performing task", Thread.currentThread().getId()), e);
+ this.logger.warn(() -> String.format("[%d] exception while consuming task", Thread.currentThread().getId()), e);
}
- if (this.isLoggerTraceEnabled) {
- this.logger.trace(() -> String.format("[%d] finished task", Thread.currentThread().getId()));
+ if (this.logger.isDebugEnabled()) {
+ final long productionPeriod = System.currentTimeMillis() - beforePerform;
+
+ this.logger.debug(() -> String.format("[%d] consumed [%s] task in %dms", Thread.currentThread().getId(), taskType, productionPeriod));
}
synchronized (this) {
++this.tasksConsumed;
--this.consumerCount;
- if (this.isLoggerTraceEnabled) {
- this.logger.trace(() -> String.format("[%d] consumerCount now: %d",
- Thread.currentThread().getId(), this.consumerCount));
- }
-
- // Quicker, non-blocking produce next round
- canBlock = false;
+ this.logger.trace(() -> String.format("[%d] consumerCount now: %d",
+ Thread.currentThread().getId(), this.consumerCount));
}
}
} finally {
- if (this.isLoggerTraceEnabled)
- Thread.currentThread().setName(this.className);
+ Thread.currentThread().setName(this.className);
}
}
diff --git a/src/main/resources/blockchain.json b/src/main/resources/blockchain.json
index e9d6a6f1..403e35bb 100644
--- a/src/main/resources/blockchain.json
+++ b/src/main/resources/blockchain.json
@@ -5,7 +5,8 @@
"maxBytesPerUnitFee": 1024,
"unitFee": "0.001",
"nameRegistrationUnitFees": [
- { "timestamp": 1645372800000, "fee": "5" }
+ { "timestamp": 1645372800000, "fee": "5" },
+ { "timestamp": 1651420800000, "fee": "1.25" }
],
"useBrokenMD160ForAddresses": false,
"requireGroupForApproval": false,
diff --git a/src/test/java/org/qortal/test/EPCTests.java b/src/test/java/org/qortal/test/EPCTests.java
index fe48af24..1a41b75d 100644
--- a/src/test/java/org/qortal/test/EPCTests.java
+++ b/src/test/java/org/qortal/test/EPCTests.java
@@ -13,9 +13,25 @@ import org.junit.Test;
import org.qortal.utils.ExecuteProduceConsume;
import org.qortal.utils.ExecuteProduceConsume.StatsSnapshot;
+import static org.junit.Assert.fail;
+
public class EPCTests {
- class RandomEPC extends ExecuteProduceConsume {
+ static class SleepTask implements ExecuteProduceConsume.Task {
+ private static final Random RANDOM = new Random();
+
+ @Override
+ public String getName() {
+ return "SleepTask";
+ }
+
+ @Override
+ public void perform() throws InterruptedException {
+ Thread.sleep(RANDOM.nextInt(500) + 100);
+ }
+ }
+
+ static class RandomEPC extends ExecuteProduceConsume {
private final int TASK_PERCENT;
private final int PAUSE_PERCENT;
@@ -37,9 +53,7 @@ public class EPCTests {
// Sometimes produce a task
if (percent < TASK_PERCENT) {
- return () -> {
- Thread.sleep(random.nextInt(500) + 100);
- };
+ return new SleepTask();
} else {
// If we don't produce a task, then maybe simulate a pause until work arrives
if (canIdle && percent < PAUSE_PERCENT)
@@ -50,45 +64,6 @@ public class EPCTests {
}
}
- private void testEPC(ExecuteProduceConsume testEPC) throws InterruptedException {
- final int runTime = 60; // seconds
- System.out.println(String.format("Testing EPC for %s seconds:", runTime));
-
- final long start = System.currentTimeMillis();
- testEPC.start();
-
- // Status reports every second (bar waiting for synchronization)
- ScheduledExecutorService statusExecutor = Executors.newSingleThreadScheduledExecutor();
-
- statusExecutor.scheduleAtFixedRate(() -> {
- final StatsSnapshot snapshot = testEPC.getStatsSnapshot();
- final long seconds = (System.currentTimeMillis() - start) / 1000L;
- System.out.print(String.format("After %d second%s, ", seconds, (seconds != 1 ? "s" : "")));
- printSnapshot(snapshot);
- }, 1L, 1L, TimeUnit.SECONDS);
-
- // Let it run for a minute
- Thread.sleep(runTime * 1000L);
- statusExecutor.shutdownNow();
-
- final long before = System.currentTimeMillis();
- testEPC.shutdown(30 * 1000);
- final long after = System.currentTimeMillis();
-
- System.out.println(String.format("Shutdown took %d milliseconds", after - before));
-
- final StatsSnapshot snapshot = testEPC.getStatsSnapshot();
- System.out.print("After shutdown, ");
- printSnapshot(snapshot);
- }
-
- private void printSnapshot(final StatsSnapshot snapshot) {
- System.out.println(String.format("threads: %d active (%d max, %d exhaustion%s), tasks: %d produced / %d consumed",
- snapshot.activeThreadCount, snapshot.greatestActiveThreadCount,
- snapshot.spawnFailures, (snapshot.spawnFailures != 1 ? "s": ""),
- snapshot.tasksProduced, snapshot.tasksConsumed));
- }
-
@Test
public void testRandomEPC() throws InterruptedException {
final int TASK_PERCENT = 25; // Produce a task this % of the time
@@ -131,18 +106,39 @@ public class EPCTests {
final int MAX_PEERS = 20;
- final List lastPings = new ArrayList<>(Collections.nCopies(MAX_PEERS, System.currentTimeMillis()));
+ final List lastPingProduced = new ArrayList<>(Collections.nCopies(MAX_PEERS, System.currentTimeMillis()));
class PingTask implements ExecuteProduceConsume.Task {
private final int peerIndex;
+ private final long lastPing;
+ private final long productionTimestamp;
+ private final String name;
- public PingTask(int peerIndex) {
+ public PingTask(int peerIndex, long lastPing, long productionTimestamp) {
this.peerIndex = peerIndex;
+ this.lastPing = lastPing;
+ this.productionTimestamp = productionTimestamp;
+ this.name = "PingTask::[" + this.peerIndex + "]";
+ }
+
+ @Override
+ public String getName() {
+ return name;
}
@Override
public void perform() throws InterruptedException {
- System.out.println("Pinging peer " + peerIndex);
+ long now = System.currentTimeMillis();
+
+ System.out.println(String.format("Pinging peer %d after post-production delay of %dms and ping interval of %dms",
+ peerIndex,
+ now - productionTimestamp,
+ now - lastPing
+ ));
+
+ long threshold = now - PING_INTERVAL - PRODUCER_SLEEP_TIME;
+ if (lastPing < threshold)
+ fail("excessive peer ping interval for peer " + peerIndex);
// At least half the worst case ping round-trip
Random random = new Random();
@@ -155,32 +151,73 @@ public class EPCTests {
class PingEPC extends ExecuteProduceConsume {
@Override
protected Task produceTask(boolean canIdle) throws InterruptedException {
- // If we can idle, then we do, to simulate worst case
- if (canIdle)
- Thread.sleep(PRODUCER_SLEEP_TIME);
-
// Is there a peer that needs a ping?
final long now = System.currentTimeMillis();
- synchronized (lastPings) {
- for (int peerIndex = 0; peerIndex < lastPings.size(); ++peerIndex) {
- long lastPing = lastPings.get(peerIndex);
-
- if (lastPing < now - PING_INTERVAL - PING_ROUND_TRIP_TIME - PRODUCER_SLEEP_TIME)
- throw new RuntimeException("excessive peer ping interval for peer " + peerIndex);
+ synchronized (lastPingProduced) {
+ for (int peerIndex = 0; peerIndex < lastPingProduced.size(); ++peerIndex) {
+ long lastPing = lastPingProduced.get(peerIndex);
if (lastPing < now - PING_INTERVAL) {
- lastPings.set(peerIndex, System.currentTimeMillis());
- return new PingTask(peerIndex);
+ lastPingProduced.set(peerIndex, System.currentTimeMillis());
+ return new PingTask(peerIndex, lastPing, now);
}
}
}
+ // If we can idle, then we do, to simulate worst case
+ if (canIdle)
+ Thread.sleep(PRODUCER_SLEEP_TIME);
+
// No work to do
return null;
}
}
+ System.out.println(String.format("Pings should start after %s seconds", PING_INTERVAL));
+
testEPC(new PingEPC());
}
+ private void testEPC(ExecuteProduceConsume testEPC) throws InterruptedException {
+ final int runTime = 60; // seconds
+ System.out.println(String.format("Testing EPC for %s seconds:", runTime));
+
+ final long start = System.currentTimeMillis();
+
+ // Status reports every second (bar waiting for synchronization)
+ ScheduledExecutorService statusExecutor = Executors.newSingleThreadScheduledExecutor();
+
+ statusExecutor.scheduleAtFixedRate(
+ () -> {
+ final StatsSnapshot snapshot = testEPC.getStatsSnapshot();
+ final long seconds = (System.currentTimeMillis() - start) / 1000L;
+ System.out.println(String.format("After %d second%s, %s", seconds, seconds != 1 ? "s" : "", formatSnapshot(snapshot)));
+ },
+ 0L, 1L, TimeUnit.SECONDS
+ );
+
+ testEPC.start();
+
+ // Let it run for a minute
+ Thread.sleep(runTime * 1000L);
+ statusExecutor.shutdownNow();
+
+ final long before = System.currentTimeMillis();
+ testEPC.shutdown(30 * 1000);
+ final long after = System.currentTimeMillis();
+
+ System.out.println(String.format("Shutdown took %d milliseconds", after - before));
+
+ final StatsSnapshot snapshot = testEPC.getStatsSnapshot();
+ System.out.println("After shutdown, " + formatSnapshot(snapshot));
+ }
+
+ private String formatSnapshot(StatsSnapshot snapshot) {
+ return String.format("threads: %d active (%d max, %d exhaustion%s), tasks: %d produced / %d consumed",
+ snapshot.activeThreadCount, snapshot.greatestActiveThreadCount,
+ snapshot.spawnFailures, (snapshot.spawnFailures != 1 ? "s": ""),
+ snapshot.tasksProduced, snapshot.tasksConsumed
+ );
+ }
+
}
diff --git a/src/test/java/org/qortal/test/crosschain/DigibyteTests.java b/src/test/java/org/qortal/test/crosschain/DigibyteTests.java
new file mode 100644
index 00000000..38dde242
--- /dev/null
+++ b/src/test/java/org/qortal/test/crosschain/DigibyteTests.java
@@ -0,0 +1,115 @@
+package org.qortal.test.crosschain;
+
+import static org.junit.Assert.*;
+
+import java.util.Arrays;
+
+import org.bitcoinj.core.Transaction;
+import org.bitcoinj.store.BlockStoreException;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.qortal.crosschain.ForeignBlockchainException;
+import org.qortal.crosschain.Digibyte;
+import org.qortal.crosschain.BitcoinyHTLC;
+import org.qortal.repository.DataException;
+import org.qortal.test.common.Common;
+
+public class DigibyteTests extends Common {
+
+ private Digibyte digibyte;
+
+ @Before
+ public void beforeTest() throws DataException {
+ Common.useDefaultSettings(); // TestNet3
+ digibyte = Digibyte.getInstance();
+ }
+
+ @After
+ public void afterTest() {
+ Digibyte.resetForTesting();
+ digibyte = null;
+ }
+
+ @Test
+ public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException {
+ long before = System.currentTimeMillis();
+ System.out.println(String.format("Digibyte median blocktime: %d", digibyte.getMedianBlockTime()));
+ long afterFirst = System.currentTimeMillis();
+
+ System.out.println(String.format("Digibyte median blocktime: %d", digibyte.getMedianBlockTime()));
+ long afterSecond = System.currentTimeMillis();
+
+ long firstPeriod = afterFirst - before;
+ long secondPeriod = afterSecond - afterFirst;
+
+ System.out.println(String.format("1st call: %d ms, 2nd call: %d ms", firstPeriod, secondPeriod));
+
+ assertTrue("2nd call should be quicker than 1st", secondPeriod < firstPeriod);
+ assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L);
+ }
+
+ @Test
+ @Ignore(value = "Doesn't work, to be fixed later")
+ public void testFindHtlcSecret() throws ForeignBlockchainException {
+ // This actually exists on TEST3 but can take a while to fetch
+ String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
+
+ byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
+ byte[] secret = BitcoinyHTLC.findHtlcSecret(digibyte, p2shAddress);
+
+ assertNotNull("secret not found", secret);
+ assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
+ }
+
+ @Test
+ @Ignore(value = "No testnet nodes available, so we can't regularly test buildSpend yet")
+ public void testBuildSpend() {
+ String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
+
+ String recipient = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
+ long amount = 1000L;
+
+ Transaction transaction = digibyte.buildSpend(xprv58, recipient, amount);
+ assertNotNull("insufficient funds", transaction);
+
+ // Check spent key caching doesn't affect outcome
+
+ transaction = digibyte.buildSpend(xprv58, recipient, amount);
+ assertNotNull("insufficient funds", transaction);
+ }
+
+ @Test
+ public void testGetWalletBalance() {
+ String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
+
+ Long balance = digibyte.getWalletBalance(xprv58);
+
+ assertNotNull(balance);
+
+ System.out.println(digibyte.format(balance));
+
+ // Check spent key caching doesn't affect outcome
+
+ Long repeatBalance = digibyte.getWalletBalance(xprv58);
+
+ assertNotNull(repeatBalance);
+
+ System.out.println(digibyte.format(repeatBalance));
+
+ assertEquals(balance, repeatBalance);
+ }
+
+ @Test
+ public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
+ String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
+
+ String address = digibyte.getUnusedReceiveAddress(xprv58);
+
+ assertNotNull(address);
+
+ System.out.println(address);
+ }
+
+}
diff --git a/src/test/java/org/qortal/test/crosschain/RavencoinTests.java b/src/test/java/org/qortal/test/crosschain/RavencoinTests.java
new file mode 100644
index 00000000..16d811dc
--- /dev/null
+++ b/src/test/java/org/qortal/test/crosschain/RavencoinTests.java
@@ -0,0 +1,115 @@
+package org.qortal.test.crosschain;
+
+import static org.junit.Assert.*;
+
+import java.util.Arrays;
+
+import org.bitcoinj.core.Transaction;
+import org.bitcoinj.store.BlockStoreException;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.qortal.crosschain.ForeignBlockchainException;
+import org.qortal.crosschain.Ravencoin;
+import org.qortal.crosschain.BitcoinyHTLC;
+import org.qortal.repository.DataException;
+import org.qortal.test.common.Common;
+
+public class RavencoinTests extends Common {
+
+ private Ravencoin ravencoin;
+
+ @Before
+ public void beforeTest() throws DataException {
+ Common.useDefaultSettings(); // TestNet3
+ ravencoin = Ravencoin.getInstance();
+ }
+
+ @After
+ public void afterTest() {
+ Ravencoin.resetForTesting();
+ ravencoin = null;
+ }
+
+ @Test
+ public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException {
+ long before = System.currentTimeMillis();
+ System.out.println(String.format("Ravencoin median blocktime: %d", ravencoin.getMedianBlockTime()));
+ long afterFirst = System.currentTimeMillis();
+
+ System.out.println(String.format("Ravencoin median blocktime: %d", ravencoin.getMedianBlockTime()));
+ long afterSecond = System.currentTimeMillis();
+
+ long firstPeriod = afterFirst - before;
+ long secondPeriod = afterSecond - afterFirst;
+
+ System.out.println(String.format("1st call: %d ms, 2nd call: %d ms", firstPeriod, secondPeriod));
+
+ assertTrue("2nd call should be quicker than 1st", secondPeriod < firstPeriod);
+ assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L);
+ }
+
+ @Test
+ @Ignore(value = "Doesn't work, to be fixed later")
+ public void testFindHtlcSecret() throws ForeignBlockchainException {
+ // This actually exists on TEST3 but can take a while to fetch
+ String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
+
+ byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
+ byte[] secret = BitcoinyHTLC.findHtlcSecret(ravencoin, p2shAddress);
+
+ assertNotNull("secret not found", secret);
+ assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
+ }
+
+ @Test
+ @Ignore(value = "No testnet nodes available, so we can't regularly test buildSpend yet")
+ public void testBuildSpend() {
+ String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
+
+ String recipient = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
+ long amount = 1000L;
+
+ Transaction transaction = ravencoin.buildSpend(xprv58, recipient, amount);
+ assertNotNull("insufficient funds", transaction);
+
+ // Check spent key caching doesn't affect outcome
+
+ transaction = ravencoin.buildSpend(xprv58, recipient, amount);
+ assertNotNull("insufficient funds", transaction);
+ }
+
+ @Test
+ public void testGetWalletBalance() {
+ String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
+
+ Long balance = ravencoin.getWalletBalance(xprv58);
+
+ assertNotNull(balance);
+
+ System.out.println(ravencoin.format(balance));
+
+ // Check spent key caching doesn't affect outcome
+
+ Long repeatBalance = ravencoin.getWalletBalance(xprv58);
+
+ assertNotNull(repeatBalance);
+
+ System.out.println(ravencoin.format(repeatBalance));
+
+ assertEquals(balance, repeatBalance);
+ }
+
+ @Test
+ public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
+ String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
+
+ String address = ravencoin.getUnusedReceiveAddress(xprv58);
+
+ assertNotNull(address);
+
+ System.out.println(address);
+ }
+
+}
diff --git a/src/test/java/org/qortal/test/crosschain/digibytev3/DigibyteACCTv3Tests.java b/src/test/java/org/qortal/test/crosschain/digibytev3/DigibyteACCTv3Tests.java
new file mode 100644
index 00000000..d13aba4c
--- /dev/null
+++ b/src/test/java/org/qortal/test/crosschain/digibytev3/DigibyteACCTv3Tests.java
@@ -0,0 +1,769 @@
+package org.qortal.test.crosschain.digibytev3;
+
+import com.google.common.hash.HashCode;
+import com.google.common.primitives.Bytes;
+import org.junit.Before;
+import org.junit.Test;
+import org.qortal.account.Account;
+import org.qortal.account.PrivateKeyAccount;
+import org.qortal.asset.Asset;
+import org.qortal.block.Block;
+import org.qortal.crosschain.AcctMode;
+import org.qortal.crosschain.DigibyteACCTv3;
+import org.qortal.crypto.Crypto;
+import org.qortal.data.at.ATData;
+import org.qortal.data.at.ATStateData;
+import org.qortal.data.crosschain.CrossChainTradeData;
+import org.qortal.data.transaction.BaseTransactionData;
+import org.qortal.data.transaction.DeployAtTransactionData;
+import org.qortal.data.transaction.MessageTransactionData;
+import org.qortal.data.transaction.TransactionData;
+import org.qortal.group.Group;
+import org.qortal.repository.DataException;
+import org.qortal.repository.Repository;
+import org.qortal.repository.RepositoryManager;
+import org.qortal.test.common.BlockUtils;
+import org.qortal.test.common.Common;
+import org.qortal.test.common.TransactionUtils;
+import org.qortal.transaction.DeployAtTransaction;
+import org.qortal.transaction.MessageTransaction;
+import org.qortal.utils.Amounts;
+
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.time.format.FormatStyle;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+import java.util.function.Function;
+
+import static org.junit.Assert.*;
+
+public class DigibyteACCTv3Tests extends Common {
+
+ public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes();
+ public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
+ public static final byte[] digibytePublicKeyHash = HashCode.fromString("bb00bb11bb22bb33bb44bb55bb66bb77bb88bb99").asBytes();
+ public static final int tradeTimeout = 20; // blocks
+ public static final long redeemAmount = 80_40200000L;
+ public static final long fundingAmount = 123_45600000L;
+ public static final long digibyteAmount = 864200L; // 0.00864200 DGB
+
+ private static final Random RANDOM = new Random();
+
+ @Before
+ public void beforeTest() throws DataException {
+ Common.useDefaultSettings();
+ }
+
+ @Test
+ public void testCompile() {
+ PrivateKeyAccount tradeAccount = createTradeAccount(null);
+
+ byte[] creationBytes = DigibyteACCTv3.buildQortalAT(tradeAccount.getAddress(), digibytePublicKeyHash, redeemAmount, digibyteAmount, tradeTimeout);
+ assertNotNull(creationBytes);
+
+ System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
+ }
+
+ @Test
+ public void testDeploy() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
+ PrivateKeyAccount tradeAccount = createTradeAccount(repository);
+
+ PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
+
+ long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
+ long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
+
+ DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
+
+ long expectedBalance = deployersInitialBalance - fundingAmount - deployAtTransaction.getTransactionData().getFee();
+ long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
+
+ assertEquals("Deployer's post-deployment balance incorrect", expectedBalance, actualBalance);
+
+ expectedBalance = fundingAmount;
+ actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
+
+ assertEquals("AT's post-deployment balance incorrect", expectedBalance, actualBalance);
+
+ expectedBalance = partnersInitialBalance;
+ actualBalance = partner.getConfirmedBalance(Asset.QORT);
+
+ assertEquals("Partner's post-deployment balance incorrect", expectedBalance, actualBalance);
+
+ // Test orphaning
+ BlockUtils.orphanLastBlock(repository);
+
+ expectedBalance = deployersInitialBalance;
+ actualBalance = deployer.getConfirmedBalance(Asset.QORT);
+
+ assertEquals("Deployer's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
+
+ expectedBalance = 0;
+ actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
+
+ assertEquals("AT's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
+
+ expectedBalance = partnersInitialBalance;
+ actualBalance = partner.getConfirmedBalance(Asset.QORT);
+
+ assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @Test
+ public void testOfferCancel() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
+ PrivateKeyAccount tradeAccount = createTradeAccount(repository);
+
+ PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
+
+ long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
+ long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
+
+ DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
+ Account at = deployAtTransaction.getATAccount();
+ String atAddress = at.getAddress();
+
+ long deployAtFee = deployAtTransaction.getTransactionData().getFee();
+ long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
+
+ // Send creator's address to AT, instead of typical partner's address
+ byte[] messageData = DigibyteACCTv3.getInstance().buildCancelMessage(deployer.getAddress());
+ MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
+ long messageFee = messageTransaction.getTransactionData().getFee();
+
+ // AT should process 'cancel' message in next block
+ BlockUtils.mintBlock(repository);
+
+ describeAt(repository, atAddress);
+
+ // Check AT is finished
+ ATData atData = repository.getATRepository().fromATAddress(atAddress);
+ assertTrue(atData.getIsFinished());
+
+ // AT should be in CANCELLED mode
+ CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
+ assertEquals(AcctMode.CANCELLED, tradeData.mode);
+
+ // Check balances
+ long expectedMinimumBalance = deployersPostDeploymentBalance;
+ long expectedMaximumBalance = deployersInitialBalance - deployAtFee - messageFee;
+
+ long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
+
+ assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
+ assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
+
+ // Test orphaning
+ BlockUtils.orphanLastBlock(repository);
+
+ // Check balances
+ long expectedBalance = deployersPostDeploymentBalance - messageFee;
+ actualBalance = deployer.getConfirmedBalance(Asset.QORT);
+
+ assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @Test
+ public void testOfferCancelInvalidLength() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
+ PrivateKeyAccount tradeAccount = createTradeAccount(repository);
+
+ PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
+
+ long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
+ long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
+
+ DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
+ Account at = deployAtTransaction.getATAccount();
+ String atAddress = at.getAddress();
+
+ long deployAtFee = deployAtTransaction.getTransactionData().getFee();
+ long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
+
+ // Instead of sending creator's address to AT, send too-short/invalid message
+ byte[] messageData = new byte[7];
+ RANDOM.nextBytes(messageData);
+ MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
+ long messageFee = messageTransaction.getTransactionData().getFee();
+
+ // AT should process 'cancel' message in next block
+ // As message is too short, it will be padded to 32bytes but cancel code doesn't care about message content, so should be ok
+ BlockUtils.mintBlock(repository);
+
+ describeAt(repository, atAddress);
+
+ // Check AT is finished
+ ATData atData = repository.getATRepository().fromATAddress(atAddress);
+ assertTrue(atData.getIsFinished());
+
+ // AT should be in CANCELLED mode
+ CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
+ assertEquals(AcctMode.CANCELLED, tradeData.mode);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @Test
+ public void testTradingInfoProcessing() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
+ PrivateKeyAccount tradeAccount = createTradeAccount(repository);
+
+ PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
+
+ long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
+ long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
+
+ DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
+ Account at = deployAtTransaction.getATAccount();
+ String atAddress = at.getAddress();
+
+ long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
+ int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
+ int refundTimeout = DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
+
+ // Send trade info to AT
+ byte[] messageData = DigibyteACCTv3.buildTradeMessage(partner.getAddress(), digibytePublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
+ MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
+
+ Block postDeploymentBlock = BlockUtils.mintBlock(repository);
+ int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
+
+ long deployAtFee = deployAtTransaction.getTransactionData().getFee();
+ long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
+
+ describeAt(repository, atAddress);
+
+ ATData atData = repository.getATRepository().fromATAddress(atAddress);
+ CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
+
+ // AT should be in TRADE mode
+ assertEquals(AcctMode.TRADING, tradeData.mode);
+
+ // Check hashOfSecretA was extracted correctly
+ assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
+
+ // Check trade partner Qortal address was extracted correctly
+ assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
+
+ // Check trade partner's digibyte PKH was extracted correctly
+ assertTrue(Arrays.equals(digibytePublicKeyHash, tradeData.partnerForeignPKH));
+
+ // Test orphaning
+ BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
+
+ // Check balances
+ long expectedBalance = deployersPostDeploymentBalance;
+ long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
+
+ assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
+ }
+ }
+
+ // TEST SENDING TRADING INFO BUT NOT FROM AT CREATOR (SHOULD BE IGNORED)
+ @SuppressWarnings("unused")
+ @Test
+ public void testIncorrectTradeSender() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
+ PrivateKeyAccount tradeAccount = createTradeAccount(repository);
+
+ PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
+
+ PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
+
+ long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
+ long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
+
+ DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
+ Account at = deployAtTransaction.getATAccount();
+ String atAddress = at.getAddress();
+
+ long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
+ int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
+ int refundTimeout = DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
+
+ // Send trade info to AT BUT NOT FROM AT CREATOR
+ byte[] messageData = DigibyteACCTv3.buildTradeMessage(partner.getAddress(), digibytePublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
+ MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
+
+ BlockUtils.mintBlock(repository);
+
+ long expectedBalance = partnersInitialBalance;
+ long actualBalance = partner.getConfirmedBalance(Asset.QORT);
+
+ assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance);
+
+ describeAt(repository, atAddress);
+
+ ATData atData = repository.getATRepository().fromATAddress(atAddress);
+ CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
+
+ // AT should still be in OFFER mode
+ assertEquals(AcctMode.OFFERING, tradeData.mode);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @Test
+ public void testAutomaticTradeRefund() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
+ PrivateKeyAccount tradeAccount = createTradeAccount(repository);
+
+ PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
+
+ long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
+ long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
+
+ DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
+ Account at = deployAtTransaction.getATAccount();
+ String atAddress = at.getAddress();
+
+ long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
+ int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
+ int refundTimeout = DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
+
+ // Send trade info to AT
+ byte[] messageData = DigibyteACCTv3.buildTradeMessage(partner.getAddress(), digibytePublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
+ MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
+
+ Block postDeploymentBlock = BlockUtils.mintBlock(repository);
+ int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
+
+ // Check refund
+ long deployAtFee = deployAtTransaction.getTransactionData().getFee();
+ long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
+
+ checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
+
+ describeAt(repository, atAddress);
+
+ // Check AT is finished
+ ATData atData = repository.getATRepository().fromATAddress(atAddress);
+ assertTrue(atData.getIsFinished());
+
+ // AT should be in REFUNDED mode
+ CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
+ assertEquals(AcctMode.REFUNDED, tradeData.mode);
+
+ // Test orphaning
+ BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
+
+ // Check balances
+ long expectedBalance = deployersPostDeploymentBalance;
+ long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
+
+ assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @Test
+ public void testCorrectSecretCorrectSender() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
+ PrivateKeyAccount tradeAccount = createTradeAccount(repository);
+
+ PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
+
+ long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
+ long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
+
+ DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
+ Account at = deployAtTransaction.getATAccount();
+ String atAddress = at.getAddress();
+
+ long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
+ int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
+ int refundTimeout = DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
+
+ // Send trade info to AT
+ byte[] messageData = DigibyteACCTv3.buildTradeMessage(partner.getAddress(), digibytePublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
+ MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
+
+ // Give AT time to process message
+ BlockUtils.mintBlock(repository);
+
+ // Send correct secret to AT, from correct account
+ messageData = DigibyteACCTv3.buildRedeemMessage(secretA, partner.getAddress());
+ messageTransaction = sendMessage(repository, partner, messageData, atAddress);
+
+ // AT should send funds in the next block
+ ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
+ BlockUtils.mintBlock(repository);
+
+ describeAt(repository, atAddress);
+
+ // Check AT is finished
+ ATData atData = repository.getATRepository().fromATAddress(atAddress);
+ assertTrue(atData.getIsFinished());
+
+ // AT should be in REDEEMED mode
+ CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
+ assertEquals(AcctMode.REDEEMED, tradeData.mode);
+
+ // Check balances
+ long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
+ long actualBalance = partner.getConfirmedBalance(Asset.QORT);
+
+ assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance);
+
+ // Orphan redeem
+ BlockUtils.orphanLastBlock(repository);
+
+ // Check balances
+ expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
+ actualBalance = partner.getConfirmedBalance(Asset.QORT);
+
+ assertEquals("Partner's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance);
+
+ // Check AT state
+ ATStateData postOrphanAtStateData = repository.getATRepository().getLatestATState(atAddress);
+
+ assertTrue("AT states mismatch", Arrays.equals(preRedeemAtStateData.getStateData(), postOrphanAtStateData.getStateData()));
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @Test
+ public void testCorrectSecretIncorrectSender() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
+ PrivateKeyAccount tradeAccount = createTradeAccount(repository);
+
+ PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
+
+ PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
+
+ long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
+ long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
+
+ DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
+ long deployAtFee = deployAtTransaction.getTransactionData().getFee();
+
+ Account at = deployAtTransaction.getATAccount();
+ String atAddress = at.getAddress();
+
+ long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
+ int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
+ int refundTimeout = DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
+
+ // Send trade info to AT
+ byte[] messageData = DigibyteACCTv3.buildTradeMessage(partner.getAddress(), digibytePublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
+ MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
+
+ // Give AT time to process message
+ BlockUtils.mintBlock(repository);
+
+ // Send correct secret to AT, but from wrong account
+ messageData = DigibyteACCTv3.buildRedeemMessage(secretA, partner.getAddress());
+ messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
+
+ // AT should NOT send funds in the next block
+ ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
+ BlockUtils.mintBlock(repository);
+
+ describeAt(repository, atAddress);
+
+ // Check AT is NOT finished
+ ATData atData = repository.getATRepository().fromATAddress(atAddress);
+ assertFalse(atData.getIsFinished());
+
+ // AT should still be in TRADE mode
+ CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
+ assertEquals(AcctMode.TRADING, tradeData.mode);
+
+ // Check balances
+ long expectedBalance = partnersInitialBalance;
+ long actualBalance = partner.getConfirmedBalance(Asset.QORT);
+
+ assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
+
+ // Check eventual refund
+ checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @Test
+ public void testIncorrectSecretCorrectSender() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
+ PrivateKeyAccount tradeAccount = createTradeAccount(repository);
+
+ PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
+
+ long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
+ long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
+
+ DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
+ long deployAtFee = deployAtTransaction.getTransactionData().getFee();
+
+ Account at = deployAtTransaction.getATAccount();
+ String atAddress = at.getAddress();
+
+ long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
+ int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
+ int refundTimeout = DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
+
+ // Send trade info to AT
+ byte[] messageData = DigibyteACCTv3.buildTradeMessage(partner.getAddress(), digibytePublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
+ MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
+
+ // Give AT time to process message
+ BlockUtils.mintBlock(repository);
+
+ // Send incorrect secret to AT, from correct account
+ byte[] wrongSecret = new byte[32];
+ RANDOM.nextBytes(wrongSecret);
+ messageData = DigibyteACCTv3.buildRedeemMessage(wrongSecret, partner.getAddress());
+ messageTransaction = sendMessage(repository, partner, messageData, atAddress);
+
+ // AT should NOT send funds in the next block
+ ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
+ BlockUtils.mintBlock(repository);
+
+ describeAt(repository, atAddress);
+
+ // Check AT is NOT finished
+ ATData atData = repository.getATRepository().fromATAddress(atAddress);
+ assertFalse(atData.getIsFinished());
+
+ // AT should still be in TRADE mode
+ CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
+ assertEquals(AcctMode.TRADING, tradeData.mode);
+
+ long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
+ long actualBalance = partner.getConfirmedBalance(Asset.QORT);
+
+ assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
+
+ // Check eventual refund
+ checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @Test
+ public void testCorrectSecretCorrectSenderInvalidMessageLength() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
+ PrivateKeyAccount tradeAccount = createTradeAccount(repository);
+
+ PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
+
+ long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
+ long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
+
+ DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
+ Account at = deployAtTransaction.getATAccount();
+ String atAddress = at.getAddress();
+
+ long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
+ int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
+ int refundTimeout = DigibyteACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
+
+ // Send trade info to AT
+ byte[] messageData = DigibyteACCTv3.buildTradeMessage(partner.getAddress(), digibytePublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
+ MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
+
+ // Give AT time to process message
+ BlockUtils.mintBlock(repository);
+
+ // Send correct secret to AT, from correct account, but missing receive address, hence incorrect length
+ messageData = Bytes.concat(secretA);
+ messageTransaction = sendMessage(repository, partner, messageData, atAddress);
+
+ // AT should NOT send funds in the next block
+ ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
+ BlockUtils.mintBlock(repository);
+
+ describeAt(repository, atAddress);
+
+ // Check AT is NOT finished
+ ATData atData = repository.getATRepository().fromATAddress(atAddress);
+ assertFalse(atData.getIsFinished());
+
+ // AT should be in TRADING mode
+ CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
+ assertEquals(AcctMode.TRADING, tradeData.mode);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @Test
+ public void testDescribeDeployed() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
+ PrivateKeyAccount tradeAccount = createTradeAccount(repository);
+
+ PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
+
+ long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
+ long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
+
+ DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
+
+ List executableAts = repository.getATRepository().getAllExecutableATs();
+
+ for (ATData atData : executableAts) {
+ String atAddress = atData.getATAddress();
+ byte[] codeBytes = atData.getCodeBytes();
+ byte[] codeHash = Crypto.digest(codeBytes);
+
+ System.out.println(String.format("%s: code length: %d byte%s, code hash: %s",
+ atAddress,
+ codeBytes.length,
+ (codeBytes.length != 1 ? "s": ""),
+ HashCode.fromBytes(codeHash)));
+
+ // Not one of ours?
+ if (!Arrays.equals(codeHash, DigibyteACCTv3.CODE_BYTES_HASH))
+ continue;
+
+ describeAt(repository, atAddress);
+ }
+ }
+ }
+
+ private int calcTestLockTimeA(long messageTimestamp) {
+ return (int) (messageTimestamp / 1000L + tradeTimeout * 60);
+ }
+
+ private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException {
+ byte[] creationBytes = DigibyteACCTv3.buildQortalAT(tradeAddress, digibytePublicKeyHash, redeemAmount, digibyteAmount, tradeTimeout);
+
+ long txTimestamp = System.currentTimeMillis();
+ byte[] lastReference = deployer.getLastReference();
+
+ if (lastReference == null) {
+ System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress()));
+ System.exit(2);
+ }
+
+ Long fee = null;
+ String name = "QORT-DGB cross-chain trade";
+ String description = String.format("Qortal-Digibyte cross-chain trade");
+ String atType = "ACCT";
+ String tags = "QORT-DGB ACCT";
+
+ BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null);
+ TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT);
+
+ DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
+
+ fee = deployAtTransaction.calcRecommendedFee();
+ deployAtTransactionData.setFee(fee);
+
+ TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer);
+
+ return deployAtTransaction;
+ }
+
+ private MessageTransaction sendMessage(Repository repository, PrivateKeyAccount sender, byte[] data, String recipient) throws DataException {
+ long txTimestamp = System.currentTimeMillis();
+ byte[] lastReference = sender.getLastReference();
+
+ if (lastReference == null) {
+ System.err.println(String.format("Qortal account %s has no last reference", sender.getAddress()));
+ System.exit(2);
+ }
+
+ Long fee = null;
+ int version = 4;
+ int nonce = 0;
+ long amount = 0;
+ Long assetId = null; // because amount is zero
+
+ BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
+ TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, data, false, false);
+
+ MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);
+
+ fee = messageTransaction.calcRecommendedFee();
+ messageTransactionData.setFee(fee);
+
+ TransactionUtils.signAndMint(repository, messageTransactionData, sender);
+
+ return messageTransaction;
+ }
+
+ private void checkTradeRefund(Repository repository, Account deployer, long deployersInitialBalance, long deployAtFee) throws DataException {
+ long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
+ int refundTimeout = tradeTimeout / 2 + 1; // close enough
+
+ // AT should automatically refund deployer after 'refundTimeout' blocks
+ for (int blockCount = 0; blockCount <= refundTimeout; ++blockCount)
+ BlockUtils.mintBlock(repository);
+
+ // We don't bother to exactly calculate QORT spent running AT for several blocks, but we do know the expected range
+ long expectedMinimumBalance = deployersPostDeploymentBalance;
+ long expectedMaximumBalance = deployersInitialBalance - deployAtFee;
+
+ long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
+
+ assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
+ assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
+ }
+
+ private void describeAt(Repository repository, String atAddress) throws DataException {
+ ATData atData = repository.getATRepository().fromATAddress(atAddress);
+ CrossChainTradeData tradeData = DigibyteACCTv3.getInstance().populateTradeData(repository, atData);
+
+ Function epochMilliFormatter = (timestamp) -> LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC).format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM));
+ int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight();
+
+ System.out.print(String.format("%s:\n"
+ + "\tmode: %s\n"
+ + "\tcreator: %s,\n"
+ + "\tcreation timestamp: %s,\n"
+ + "\tcurrent balance: %s QORT,\n"
+ + "\tis finished: %b,\n"
+ + "\tredeem payout: %s QORT,\n"
+ + "\texpected digibyte: %s DGB,\n"
+ + "\tcurrent block height: %d,\n",
+ tradeData.qortalAtAddress,
+ tradeData.mode,
+ tradeData.qortalCreator,
+ epochMilliFormatter.apply(tradeData.creationTimestamp),
+ Amounts.prettyAmount(tradeData.qortBalance),
+ atData.getIsFinished(),
+ Amounts.prettyAmount(tradeData.qortAmount),
+ Amounts.prettyAmount(tradeData.expectedForeignAmount),
+ currentBlockHeight));
+
+ if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
+ System.out.println(String.format("\trefund timeout: %d minutes,\n"
+ + "\trefund height: block %d,\n"
+ + "\tHASH160 of secret-A: %s,\n"
+ + "\tDigibyte P2SH-A nLockTime: %d (%s),\n"
+ + "\ttrade partner: %s\n"
+ + "\tpartner's receiving address: %s",
+ tradeData.refundTimeout,
+ tradeData.tradeRefundHeight,
+ HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40),
+ tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L),
+ tradeData.qortalPartnerAddress,
+ tradeData.qortalPartnerReceivingAddress));
+ }
+ }
+
+ private PrivateKeyAccount createTradeAccount(Repository repository) {
+ // We actually use a known test account with funds to avoid PoW compute
+ return Common.getTestAccount(repository, "alice");
+ }
+
+}
diff --git a/src/test/java/org/qortal/test/crosschain/ravencoinv3/RavencoinACCTv3Tests.java b/src/test/java/org/qortal/test/crosschain/ravencoinv3/RavencoinACCTv3Tests.java
new file mode 100644
index 00000000..012d5f5d
--- /dev/null
+++ b/src/test/java/org/qortal/test/crosschain/ravencoinv3/RavencoinACCTv3Tests.java
@@ -0,0 +1,769 @@
+package org.qortal.test.crosschain.ravencoinv3;
+
+import com.google.common.hash.HashCode;
+import com.google.common.primitives.Bytes;
+import org.junit.Before;
+import org.junit.Test;
+import org.qortal.account.Account;
+import org.qortal.account.PrivateKeyAccount;
+import org.qortal.asset.Asset;
+import org.qortal.block.Block;
+import org.qortal.crosschain.AcctMode;
+import org.qortal.crosschain.RavencoinACCTv3;
+import org.qortal.crypto.Crypto;
+import org.qortal.data.at.ATData;
+import org.qortal.data.at.ATStateData;
+import org.qortal.data.crosschain.CrossChainTradeData;
+import org.qortal.data.transaction.BaseTransactionData;
+import org.qortal.data.transaction.DeployAtTransactionData;
+import org.qortal.data.transaction.MessageTransactionData;
+import org.qortal.data.transaction.TransactionData;
+import org.qortal.group.Group;
+import org.qortal.repository.DataException;
+import org.qortal.repository.Repository;
+import org.qortal.repository.RepositoryManager;
+import org.qortal.test.common.BlockUtils;
+import org.qortal.test.common.Common;
+import org.qortal.test.common.TransactionUtils;
+import org.qortal.transaction.DeployAtTransaction;
+import org.qortal.transaction.MessageTransaction;
+import org.qortal.utils.Amounts;
+
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.time.format.FormatStyle;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+import java.util.function.Function;
+
+import static org.junit.Assert.*;
+
+public class RavencoinACCTv3Tests extends Common {
+
+ public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes();
+ public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a
+ public static final byte[] ravencoinPublicKeyHash = HashCode.fromString("bb00bb11bb22bb33bb44bb55bb66bb77bb88bb99").asBytes();
+ public static final int tradeTimeout = 20; // blocks
+ public static final long redeemAmount = 80_40200000L;
+ public static final long fundingAmount = 123_45600000L;
+ public static final long ravencoinAmount = 864200L; // 0.00864200 RVN
+
+ private static final Random RANDOM = new Random();
+
+ @Before
+ public void beforeTest() throws DataException {
+ Common.useDefaultSettings();
+ }
+
+ @Test
+ public void testCompile() {
+ PrivateKeyAccount tradeAccount = createTradeAccount(null);
+
+ byte[] creationBytes = RavencoinACCTv3.buildQortalAT(tradeAccount.getAddress(), ravencoinPublicKeyHash, redeemAmount, ravencoinAmount, tradeTimeout);
+ assertNotNull(creationBytes);
+
+ System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString());
+ }
+
+ @Test
+ public void testDeploy() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
+ PrivateKeyAccount tradeAccount = createTradeAccount(repository);
+
+ PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
+
+ long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
+ long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
+
+ DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
+
+ long expectedBalance = deployersInitialBalance - fundingAmount - deployAtTransaction.getTransactionData().getFee();
+ long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
+
+ assertEquals("Deployer's post-deployment balance incorrect", expectedBalance, actualBalance);
+
+ expectedBalance = fundingAmount;
+ actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
+
+ assertEquals("AT's post-deployment balance incorrect", expectedBalance, actualBalance);
+
+ expectedBalance = partnersInitialBalance;
+ actualBalance = partner.getConfirmedBalance(Asset.QORT);
+
+ assertEquals("Partner's post-deployment balance incorrect", expectedBalance, actualBalance);
+
+ // Test orphaning
+ BlockUtils.orphanLastBlock(repository);
+
+ expectedBalance = deployersInitialBalance;
+ actualBalance = deployer.getConfirmedBalance(Asset.QORT);
+
+ assertEquals("Deployer's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
+
+ expectedBalance = 0;
+ actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT);
+
+ assertEquals("AT's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
+
+ expectedBalance = partnersInitialBalance;
+ actualBalance = partner.getConfirmedBalance(Asset.QORT);
+
+ assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @Test
+ public void testOfferCancel() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
+ PrivateKeyAccount tradeAccount = createTradeAccount(repository);
+
+ PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
+
+ long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
+ long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
+
+ DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
+ Account at = deployAtTransaction.getATAccount();
+ String atAddress = at.getAddress();
+
+ long deployAtFee = deployAtTransaction.getTransactionData().getFee();
+ long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
+
+ // Send creator's address to AT, instead of typical partner's address
+ byte[] messageData = RavencoinACCTv3.getInstance().buildCancelMessage(deployer.getAddress());
+ MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
+ long messageFee = messageTransaction.getTransactionData().getFee();
+
+ // AT should process 'cancel' message in next block
+ BlockUtils.mintBlock(repository);
+
+ describeAt(repository, atAddress);
+
+ // Check AT is finished
+ ATData atData = repository.getATRepository().fromATAddress(atAddress);
+ assertTrue(atData.getIsFinished());
+
+ // AT should be in CANCELLED mode
+ CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
+ assertEquals(AcctMode.CANCELLED, tradeData.mode);
+
+ // Check balances
+ long expectedMinimumBalance = deployersPostDeploymentBalance;
+ long expectedMaximumBalance = deployersInitialBalance - deployAtFee - messageFee;
+
+ long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
+
+ assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
+ assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
+
+ // Test orphaning
+ BlockUtils.orphanLastBlock(repository);
+
+ // Check balances
+ long expectedBalance = deployersPostDeploymentBalance - messageFee;
+ actualBalance = deployer.getConfirmedBalance(Asset.QORT);
+
+ assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @Test
+ public void testOfferCancelInvalidLength() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
+ PrivateKeyAccount tradeAccount = createTradeAccount(repository);
+
+ PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
+
+ long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
+ long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
+
+ DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
+ Account at = deployAtTransaction.getATAccount();
+ String atAddress = at.getAddress();
+
+ long deployAtFee = deployAtTransaction.getTransactionData().getFee();
+ long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
+
+ // Instead of sending creator's address to AT, send too-short/invalid message
+ byte[] messageData = new byte[7];
+ RANDOM.nextBytes(messageData);
+ MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress);
+ long messageFee = messageTransaction.getTransactionData().getFee();
+
+ // AT should process 'cancel' message in next block
+ // As message is too short, it will be padded to 32bytes but cancel code doesn't care about message content, so should be ok
+ BlockUtils.mintBlock(repository);
+
+ describeAt(repository, atAddress);
+
+ // Check AT is finished
+ ATData atData = repository.getATRepository().fromATAddress(atAddress);
+ assertTrue(atData.getIsFinished());
+
+ // AT should be in CANCELLED mode
+ CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
+ assertEquals(AcctMode.CANCELLED, tradeData.mode);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @Test
+ public void testTradingInfoProcessing() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
+ PrivateKeyAccount tradeAccount = createTradeAccount(repository);
+
+ PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
+
+ long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
+ long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
+
+ DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
+ Account at = deployAtTransaction.getATAccount();
+ String atAddress = at.getAddress();
+
+ long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
+ int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
+ int refundTimeout = RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
+
+ // Send trade info to AT
+ byte[] messageData = RavencoinACCTv3.buildTradeMessage(partner.getAddress(), ravencoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
+ MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
+
+ Block postDeploymentBlock = BlockUtils.mintBlock(repository);
+ int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
+
+ long deployAtFee = deployAtTransaction.getTransactionData().getFee();
+ long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
+
+ describeAt(repository, atAddress);
+
+ ATData atData = repository.getATRepository().fromATAddress(atAddress);
+ CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
+
+ // AT should be in TRADE mode
+ assertEquals(AcctMode.TRADING, tradeData.mode);
+
+ // Check hashOfSecretA was extracted correctly
+ assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA));
+
+ // Check trade partner Qortal address was extracted correctly
+ assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress);
+
+ // Check trade partner's ravencoin PKH was extracted correctly
+ assertTrue(Arrays.equals(ravencoinPublicKeyHash, tradeData.partnerForeignPKH));
+
+ // Test orphaning
+ BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
+
+ // Check balances
+ long expectedBalance = deployersPostDeploymentBalance;
+ long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
+
+ assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
+ }
+ }
+
+ // TEST SENDING TRADING INFO BUT NOT FROM AT CREATOR (SHOULD BE IGNORED)
+ @SuppressWarnings("unused")
+ @Test
+ public void testIncorrectTradeSender() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
+ PrivateKeyAccount tradeAccount = createTradeAccount(repository);
+
+ PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
+
+ PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
+
+ long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
+ long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
+
+ DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
+ Account at = deployAtTransaction.getATAccount();
+ String atAddress = at.getAddress();
+
+ long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
+ int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
+ int refundTimeout = RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
+
+ // Send trade info to AT BUT NOT FROM AT CREATOR
+ byte[] messageData = RavencoinACCTv3.buildTradeMessage(partner.getAddress(), ravencoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
+ MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
+
+ BlockUtils.mintBlock(repository);
+
+ long expectedBalance = partnersInitialBalance;
+ long actualBalance = partner.getConfirmedBalance(Asset.QORT);
+
+ assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance);
+
+ describeAt(repository, atAddress);
+
+ ATData atData = repository.getATRepository().fromATAddress(atAddress);
+ CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
+
+ // AT should still be in OFFER mode
+ assertEquals(AcctMode.OFFERING, tradeData.mode);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @Test
+ public void testAutomaticTradeRefund() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
+ PrivateKeyAccount tradeAccount = createTradeAccount(repository);
+
+ PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
+
+ long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
+ long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
+
+ DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
+ Account at = deployAtTransaction.getATAccount();
+ String atAddress = at.getAddress();
+
+ long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
+ int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
+ int refundTimeout = RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
+
+ // Send trade info to AT
+ byte[] messageData = RavencoinACCTv3.buildTradeMessage(partner.getAddress(), ravencoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
+ MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
+
+ Block postDeploymentBlock = BlockUtils.mintBlock(repository);
+ int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight();
+
+ // Check refund
+ long deployAtFee = deployAtTransaction.getTransactionData().getFee();
+ long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
+
+ checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
+
+ describeAt(repository, atAddress);
+
+ // Check AT is finished
+ ATData atData = repository.getATRepository().fromATAddress(atAddress);
+ assertTrue(atData.getIsFinished());
+
+ // AT should be in REFUNDED mode
+ CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
+ assertEquals(AcctMode.REFUNDED, tradeData.mode);
+
+ // Test orphaning
+ BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight);
+
+ // Check balances
+ long expectedBalance = deployersPostDeploymentBalance;
+ long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
+
+ assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @Test
+ public void testCorrectSecretCorrectSender() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
+ PrivateKeyAccount tradeAccount = createTradeAccount(repository);
+
+ PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
+
+ long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
+ long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
+
+ DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
+ Account at = deployAtTransaction.getATAccount();
+ String atAddress = at.getAddress();
+
+ long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
+ int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
+ int refundTimeout = RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
+
+ // Send trade info to AT
+ byte[] messageData = RavencoinACCTv3.buildTradeMessage(partner.getAddress(), ravencoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
+ MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
+
+ // Give AT time to process message
+ BlockUtils.mintBlock(repository);
+
+ // Send correct secret to AT, from correct account
+ messageData = RavencoinACCTv3.buildRedeemMessage(secretA, partner.getAddress());
+ messageTransaction = sendMessage(repository, partner, messageData, atAddress);
+
+ // AT should send funds in the next block
+ ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
+ BlockUtils.mintBlock(repository);
+
+ describeAt(repository, atAddress);
+
+ // Check AT is finished
+ ATData atData = repository.getATRepository().fromATAddress(atAddress);
+ assertTrue(atData.getIsFinished());
+
+ // AT should be in REDEEMED mode
+ CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
+ assertEquals(AcctMode.REDEEMED, tradeData.mode);
+
+ // Check balances
+ long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount;
+ long actualBalance = partner.getConfirmedBalance(Asset.QORT);
+
+ assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance);
+
+ // Orphan redeem
+ BlockUtils.orphanLastBlock(repository);
+
+ // Check balances
+ expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
+ actualBalance = partner.getConfirmedBalance(Asset.QORT);
+
+ assertEquals("Partner's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance);
+
+ // Check AT state
+ ATStateData postOrphanAtStateData = repository.getATRepository().getLatestATState(atAddress);
+
+ assertTrue("AT states mismatch", Arrays.equals(preRedeemAtStateData.getStateData(), postOrphanAtStateData.getStateData()));
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @Test
+ public void testCorrectSecretIncorrectSender() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
+ PrivateKeyAccount tradeAccount = createTradeAccount(repository);
+
+ PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
+
+ PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob");
+
+ long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
+ long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
+
+ DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
+ long deployAtFee = deployAtTransaction.getTransactionData().getFee();
+
+ Account at = deployAtTransaction.getATAccount();
+ String atAddress = at.getAddress();
+
+ long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
+ int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
+ int refundTimeout = RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
+
+ // Send trade info to AT
+ byte[] messageData = RavencoinACCTv3.buildTradeMessage(partner.getAddress(), ravencoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
+ MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
+
+ // Give AT time to process message
+ BlockUtils.mintBlock(repository);
+
+ // Send correct secret to AT, but from wrong account
+ messageData = RavencoinACCTv3.buildRedeemMessage(secretA, partner.getAddress());
+ messageTransaction = sendMessage(repository, bystander, messageData, atAddress);
+
+ // AT should NOT send funds in the next block
+ ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
+ BlockUtils.mintBlock(repository);
+
+ describeAt(repository, atAddress);
+
+ // Check AT is NOT finished
+ ATData atData = repository.getATRepository().fromATAddress(atAddress);
+ assertFalse(atData.getIsFinished());
+
+ // AT should still be in TRADE mode
+ CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
+ assertEquals(AcctMode.TRADING, tradeData.mode);
+
+ // Check balances
+ long expectedBalance = partnersInitialBalance;
+ long actualBalance = partner.getConfirmedBalance(Asset.QORT);
+
+ assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
+
+ // Check eventual refund
+ checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @Test
+ public void testIncorrectSecretCorrectSender() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
+ PrivateKeyAccount tradeAccount = createTradeAccount(repository);
+
+ PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
+
+ long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
+ long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
+
+ DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
+ long deployAtFee = deployAtTransaction.getTransactionData().getFee();
+
+ Account at = deployAtTransaction.getATAccount();
+ String atAddress = at.getAddress();
+
+ long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
+ int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
+ int refundTimeout = RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
+
+ // Send trade info to AT
+ byte[] messageData = RavencoinACCTv3.buildTradeMessage(partner.getAddress(), ravencoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
+ MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
+
+ // Give AT time to process message
+ BlockUtils.mintBlock(repository);
+
+ // Send incorrect secret to AT, from correct account
+ byte[] wrongSecret = new byte[32];
+ RANDOM.nextBytes(wrongSecret);
+ messageData = RavencoinACCTv3.buildRedeemMessage(wrongSecret, partner.getAddress());
+ messageTransaction = sendMessage(repository, partner, messageData, atAddress);
+
+ // AT should NOT send funds in the next block
+ ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
+ BlockUtils.mintBlock(repository);
+
+ describeAt(repository, atAddress);
+
+ // Check AT is NOT finished
+ ATData atData = repository.getATRepository().fromATAddress(atAddress);
+ assertFalse(atData.getIsFinished());
+
+ // AT should still be in TRADE mode
+ CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
+ assertEquals(AcctMode.TRADING, tradeData.mode);
+
+ long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee();
+ long actualBalance = partner.getConfirmedBalance(Asset.QORT);
+
+ assertEquals("Partner's balance incorrect", expectedBalance, actualBalance);
+
+ // Check eventual refund
+ checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @Test
+ public void testCorrectSecretCorrectSenderInvalidMessageLength() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
+ PrivateKeyAccount tradeAccount = createTradeAccount(repository);
+
+ PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
+
+ long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
+ long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
+
+ DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
+ Account at = deployAtTransaction.getATAccount();
+ String atAddress = at.getAddress();
+
+ long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis();
+ int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp);
+ int refundTimeout = RavencoinACCTv3.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA);
+
+ // Send trade info to AT
+ byte[] messageData = RavencoinACCTv3.buildTradeMessage(partner.getAddress(), ravencoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
+ MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress);
+
+ // Give AT time to process message
+ BlockUtils.mintBlock(repository);
+
+ // Send correct secret to AT, from correct account, but missing receive address, hence incorrect length
+ messageData = Bytes.concat(secretA);
+ messageTransaction = sendMessage(repository, partner, messageData, atAddress);
+
+ // AT should NOT send funds in the next block
+ ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress);
+ BlockUtils.mintBlock(repository);
+
+ describeAt(repository, atAddress);
+
+ // Check AT is NOT finished
+ ATData atData = repository.getATRepository().fromATAddress(atAddress);
+ assertFalse(atData.getIsFinished());
+
+ // AT should be in TRADING mode
+ CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
+ assertEquals(AcctMode.TRADING, tradeData.mode);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @Test
+ public void testDescribeDeployed() throws DataException {
+ try (final Repository repository = RepositoryManager.getRepository()) {
+ PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe");
+ PrivateKeyAccount tradeAccount = createTradeAccount(repository);
+
+ PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert");
+
+ long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT);
+ long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT);
+
+ DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress());
+
+ List executableAts = repository.getATRepository().getAllExecutableATs();
+
+ for (ATData atData : executableAts) {
+ String atAddress = atData.getATAddress();
+ byte[] codeBytes = atData.getCodeBytes();
+ byte[] codeHash = Crypto.digest(codeBytes);
+
+ System.out.println(String.format("%s: code length: %d byte%s, code hash: %s",
+ atAddress,
+ codeBytes.length,
+ (codeBytes.length != 1 ? "s": ""),
+ HashCode.fromBytes(codeHash)));
+
+ // Not one of ours?
+ if (!Arrays.equals(codeHash, RavencoinACCTv3.CODE_BYTES_HASH))
+ continue;
+
+ describeAt(repository, atAddress);
+ }
+ }
+ }
+
+ private int calcTestLockTimeA(long messageTimestamp) {
+ return (int) (messageTimestamp / 1000L + tradeTimeout * 60);
+ }
+
+ private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException {
+ byte[] creationBytes = RavencoinACCTv3.buildQortalAT(tradeAddress, ravencoinPublicKeyHash, redeemAmount, ravencoinAmount, tradeTimeout);
+
+ long txTimestamp = System.currentTimeMillis();
+ byte[] lastReference = deployer.getLastReference();
+
+ if (lastReference == null) {
+ System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress()));
+ System.exit(2);
+ }
+
+ Long fee = null;
+ String name = "QORT-RVN cross-chain trade";
+ String description = String.format("Qortal-Ravencoin cross-chain trade");
+ String atType = "ACCT";
+ String tags = "QORT-RVN ACCT";
+
+ BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null);
+ TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT);
+
+ DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
+
+ fee = deployAtTransaction.calcRecommendedFee();
+ deployAtTransactionData.setFee(fee);
+
+ TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer);
+
+ return deployAtTransaction;
+ }
+
+ private MessageTransaction sendMessage(Repository repository, PrivateKeyAccount sender, byte[] data, String recipient) throws DataException {
+ long txTimestamp = System.currentTimeMillis();
+ byte[] lastReference = sender.getLastReference();
+
+ if (lastReference == null) {
+ System.err.println(String.format("Qortal account %s has no last reference", sender.getAddress()));
+ System.exit(2);
+ }
+
+ Long fee = null;
+ int version = 4;
+ int nonce = 0;
+ long amount = 0;
+ Long assetId = null; // because amount is zero
+
+ BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null);
+ TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, data, false, false);
+
+ MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);
+
+ fee = messageTransaction.calcRecommendedFee();
+ messageTransactionData.setFee(fee);
+
+ TransactionUtils.signAndMint(repository, messageTransactionData, sender);
+
+ return messageTransaction;
+ }
+
+ private void checkTradeRefund(Repository repository, Account deployer, long deployersInitialBalance, long deployAtFee) throws DataException {
+ long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee;
+ int refundTimeout = tradeTimeout / 2 + 1; // close enough
+
+ // AT should automatically refund deployer after 'refundTimeout' blocks
+ for (int blockCount = 0; blockCount <= refundTimeout; ++blockCount)
+ BlockUtils.mintBlock(repository);
+
+ // We don't bother to exactly calculate QORT spent running AT for several blocks, but we do know the expected range
+ long expectedMinimumBalance = deployersPostDeploymentBalance;
+ long expectedMaximumBalance = deployersInitialBalance - deployAtFee;
+
+ long actualBalance = deployer.getConfirmedBalance(Asset.QORT);
+
+ assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance);
+ assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance);
+ }
+
+ private void describeAt(Repository repository, String atAddress) throws DataException {
+ ATData atData = repository.getATRepository().fromATAddress(atAddress);
+ CrossChainTradeData tradeData = RavencoinACCTv3.getInstance().populateTradeData(repository, atData);
+
+ Function epochMilliFormatter = (timestamp) -> LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC).format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM));
+ int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight();
+
+ System.out.print(String.format("%s:\n"
+ + "\tmode: %s\n"
+ + "\tcreator: %s,\n"
+ + "\tcreation timestamp: %s,\n"
+ + "\tcurrent balance: %s QORT,\n"
+ + "\tis finished: %b,\n"
+ + "\tredeem payout: %s QORT,\n"
+ + "\texpected ravencoin: %s RVN,\n"
+ + "\tcurrent block height: %d,\n",
+ tradeData.qortalAtAddress,
+ tradeData.mode,
+ tradeData.qortalCreator,
+ epochMilliFormatter.apply(tradeData.creationTimestamp),
+ Amounts.prettyAmount(tradeData.qortBalance),
+ atData.getIsFinished(),
+ Amounts.prettyAmount(tradeData.qortAmount),
+ Amounts.prettyAmount(tradeData.expectedForeignAmount),
+ currentBlockHeight));
+
+ if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) {
+ System.out.println(String.format("\trefund timeout: %d minutes,\n"
+ + "\trefund height: block %d,\n"
+ + "\tHASH160 of secret-A: %s,\n"
+ + "\tRavencoin P2SH-A nLockTime: %d (%s),\n"
+ + "\ttrade partner: %s\n"
+ + "\tpartner's receiving address: %s",
+ tradeData.refundTimeout,
+ tradeData.tradeRefundHeight,
+ HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40),
+ tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L),
+ tradeData.qortalPartnerAddress,
+ tradeData.qortalPartnerReceivingAddress));
+ }
+ }
+
+ private PrivateKeyAccount createTradeAccount(Repository repository) {
+ // We actually use a known test account with funds to avoid PoW compute
+ return Common.getTestAccount(repository, "alice");
+ }
+
+}
diff --git a/src/test/java/org/qortal/test/naming/MiscTests.java b/src/test/java/org/qortal/test/naming/MiscTests.java
index 8252453c..2bcd098d 100644
--- a/src/test/java/org/qortal/test/naming/MiscTests.java
+++ b/src/test/java/org/qortal/test/naming/MiscTests.java
@@ -356,8 +356,15 @@ public class MiscTests extends Common {
UnitFeesByTimestamp pastFeeIncrease = new UnitFeesByTimestamp();
pastFeeIncrease.timestamp = now - 1000L; // 1 second ago
pastFeeIncrease.fee = new AmountTypeAdapter().unmarshal("3");
- FieldUtils.writeField(BlockChain.getInstance(), "nameRegistrationUnitFees", Arrays.asList(pastFeeIncrease), true);
+
+ // Set another increase in the future
+ futureFeeIncrease = new UnitFeesByTimestamp();
+ futureFeeIncrease.timestamp = now + (60 * 60 * 1000L); // 1 hour in the future
+ futureFeeIncrease.fee = new AmountTypeAdapter().unmarshal("10");
+
+ FieldUtils.writeField(BlockChain.getInstance(), "nameRegistrationUnitFees", Arrays.asList(pastFeeIncrease, futureFeeIncrease), true);
assertEquals(pastFeeIncrease.fee, BlockChain.getInstance().getNameRegistrationUnitFeeAtTimestamp(pastFeeIncrease.timestamp));
+ assertEquals(futureFeeIncrease.fee, BlockChain.getInstance().getNameRegistrationUnitFeeAtTimestamp(futureFeeIncrease.timestamp));
// Register a different name
// First try with the default unit fee
diff --git a/src/test/java/org/qortal/test/network/OnlineAccountsTests.java b/src/test/java/org/qortal/test/network/OnlineAccountsTests.java
index 478709af..2b836461 100644
--- a/src/test/java/org/qortal/test/network/OnlineAccountsTests.java
+++ b/src/test/java/org/qortal/test/network/OnlineAccountsTests.java
@@ -49,7 +49,7 @@ public class OnlineAccountsTests extends Common {
@Test
- public void testGetOnlineAccountsV2() throws Message.MessageException {
+ public void testGetOnlineAccountsV2() throws MessageException {
List onlineAccountsOut = generateOnlineAccounts(false);
Message messageOut = new GetOnlineAccountsV2Message(onlineAccountsOut);
@@ -66,7 +66,7 @@ public class OnlineAccountsTests extends Common {
}
@Test
- public void testOnlineAccountsV2() throws Message.MessageException {
+ public void testOnlineAccountsV2() throws MessageException {
List onlineAccountsOut = generateOnlineAccounts(true);
Message messageOut = new OnlineAccountsV2Message(onlineAccountsOut);